jueves, enero 03, 2008

El objeto activo

¿Qué se supone que tiene que pasar cuando se ejecute este código?
using System;
public
Program = class
public
method Test: Boolean => Self <> nil;
        static method Main;
begin
var Instance: Program := nil;
Console.WriteLine(Instance.Test);
end;
end
end

Janie's got a gunLa respuesta breve es que se imprimirá False.
La respuesta larga tiene que ver con uno de los objetivos de Freya: experimentar con la eficiencia del lenguaje, y en este caso, entra en escena el código IL que se genera para la llamada a un procedimiento. Hay dos de estos códigos: call y callvirt. El segundo de ellos se utiliza, evidentemente, para ejecutar métodos virtuales, pero curiosamente, también puede ocuparse de métodos no virtuales, como es el caso de nuestro método Test. En tal caso, la diferencia respecto a call es que, al generarse el código nativo final, callvirt añade una verificación del puntero de instancia. Si este es nulo, excepción al canto.
Resulta que callvirt es utilizado deliberadamente por C# para casi todas las llamadas a métodos de instancia, sean virtuales o no. Esta es la forma de satisfacer la especificación del lenguaje, que exige que rueden cabezas si se pasa un nulo como objeto activo a un método de instancias. Es también interesante que, en contadas ocasiones, C# puede optimizar la llamada si sabe a ciencia cierta que el puntero no será nulo. ¿Cuándo puede tener esta certeza? Por ejemplo, cuando el objeto pasado se acaba de construir. Un método ejecutado sobre una expresión new no necesita esta verificación.
Freya, por el contrario, siempre usa call para métodos no virtuales, con lo que logra una ganancia en velocidad mínima, pero notable, sobre C#. Hay una ganancia también en el tamaño del código nativo, que se traduce en un uso más eficiente de la memoria, y a la larga, en más velocidad: la comprobación de no nulidad la ejecuta quien llama, por lo que se repite innumerables veces a lo largo del código. El coste: la especificación de Freya no exige que la instancia activa sea no nula. De ahí que tengan sentido ejemplos como el mostrado.
La pregunta es, naturalmente, si es peligrosa esta "relajación". No lo es, por supuesto:
  1. Si se pasa una referencia nula como objeto activo a un método que no lo permite, la excepción se va a producir, de todos modos, en cuanto el objeto activo intente tocar algún campo o método de instancia. Es decir: el error se detectará, aunque algo más tarde.
  2. En realidad, la no nulidad del objeto activo puede verse como una precondición en el contrato del método, en los lenguajes que la exigen: se supone que el software correctamente escrito nunca violará la precondición. Pero en ese caso, si en el software final se mantiene la verificación, ¡estaremos comprobando una y otra vez si se produce lo imposible!
A mediano plazo, Freya deberá incorporar aserciones especiales sobre la nulidad de campos y parámetros. Esto traerá algunas novedades interesantes al lenguaje. Piense un momento: si digo que el campo C, de tipo Object, no puede ser nulo, ¿cómo vamos a tratar el hecho de que el CLR lo inicialice precisamente como nulo? La clave está en añadir inicializadores de campos a los constructores, al estilo C++:
// En una clase Stack[X]...
constructor(Capacity: Integer) :
Items(new Array[X](Capacity)),
Inherited
begin
end
;
El CLR permite que estas inicializaciones de campos se ejecuten incluso antes de la llamada al constructor de la clase base: es así, de hecho, como se ejecutan los actuales inicializadores de campos de C# y Freya. La diferencia es que, con el nuevo recurso, los inicializadores de campos podrán utilizar los parámetros del constructor, algo que ahora mismo es imposible.
Por cierto, el último listado puede abreviarse a lo siguiente:
constructor(Capacity: Integer) :
Items(new Array[X](Capacity));
La llamada al constructor por omisión heredado se asume, y el bloque de instrucciones, al estar vacío, se puede omitir. Un constructor paralelo, sin parámetros, se programa en Freya de este modo:
constructor : Self(128);
Las instancias especiales base y this de C# se sustituyen en Freya por los identificadores especiales Inherited y Self.

... quien haya depurado una aplicación .NET en código nativo, se habrá encontrado con instrucciones aparentemente absurdas como ésta, justo antes de una llamada a rutina:
cmp byte ptr [rax],0
Se trata de la verificación de no nulidad, en este caso, en código de 64 bits. En un procesador de 32 bits, he visto esta variante:
cmp byte ptr [eax],eax
Al parecer, la diferencia debe estar en el tamaño de ambas instrucciones. Pero en ambos casos, el principio es el mismo: si EAX o RAX tienen un puntero no nulo, la instrucción hará una comparación rápida e inofensiva. En caso contrario, la memoria a la que apunta el registro estará en la zona prohibida, y el propio procesador levantará la liebre... quiero decir, la excepción.

Por último, la posibilidad de simular el traspaso de un objeto activo nulo la dan los métodos de extensión:
public static class StringExtender
{
public static bool IsEmpty(this string s)
{
return string.IsNullOrEmpty(s);
}
}
Si se puede lograr por medio de métodos de extensión, ¿qué hay de malo en permitirlo para los métodos de instancia?

Etiquetas: , ,

4 Comments:

Blogger Marto said...

Es demasiado evidente que va dar un null pointer exception (o la versión freyana).... ¿con qué nos sorprendes esta vez?

viernes, enero 04, 2008 9:14:00 a. m.  
Blogger Barullo said...

¿Lanzar una excepción por no cumplirse la precondición?

viernes, enero 04, 2008 1:54:00 p. m.  
Blogger Ian Marteens said...

En Freya, se imprime false.

Barullo: el "=>" en Freya, imitando la sintaxis de las expresiones lambda, sirve para introducir la implementación, cuando está sólo consiste en una asignación a Result. La alternativa sería más larga:

method Test: Boolean;
begin
Result := Self <> nil;
end;

viernes, enero 04, 2008 2:48:00 p. m.  
Blogger Ian Marteens said...

No estoy convencido de que deba quedarse así: C# especifica claramente que debe producirse la excepción. Sin embargo, en realidad no hay motivo para ello: el método no es virtual. Si lo fuese, sí se dispararía la excepción. Con este comportamiento, se podrían definir métodos como String.IsNullOrEmpty, que ahora es estático, como método de instancia:

method IsNullOrEmpty: Boolean =>
Self = nil or Length = 0;

Todo esto viene por otra "característica" de los métodos de extensión. Luego cuento la historia completa...

viernes, enero 04, 2008 2:51:00 p. m.  

Publicar un comentario

<< Home