Fernando さんのプロフィールFernando's spaceフォトブログリストその他 ![]() | ヘルプ |
|
|
4月25日 Demos de Visual Studio 2010 y C# 4.0: Covarianza y contravarianzaCovarianza y contravarianza (voy a usar varianza cuando me refiera a los dos al mismo tiempo) son dos conceptos bastante difíciles de entender sobre teoría de tipos de datos en un lenguaje de programación. En este artículo y en la demostración voy a usar un enfoque muy práctico para mostrar de qué hablamos cuando hablamos de varianza. Al final voy a dar las definiciones formales, sólo para que el artículo quede completo. Varianza fue uno de los temas de la charla del CUMUY de abril de 2009 sobre las novedades de Visual Studio 2010 y C# 4.0. Toda string es un object, es decir, todo lo que puedo hacer con un object (invocar métodos, consumir propiedades, asignar, etc.), puedo hacerlo también con una string, ¿correcto? Correcto. Entonces un string[] (array de strings) debería ser un objects[] (array de objects), es decir, todo lo que puedo hacer con un object[] lo puedo hacer con un string[], ¿correcto? Eh… bueno, la opinión no es unánime, el compilador de C# piensa una cosa y el CLR piensa otra. Vean el siguiente fragmento de código: 1: string[] sa = new string[10]; 2: object[] oa; 3: oa = sa; 4: oa[0] = "Hello world!“; 5: oa[1] = 5;En la línea 3, asignamos a una variable declarada como object[] un objeto creado como string[]. Luego en la línea 4 asignamos una string, que es un object, a un elemento del object[]. Por último, en la línea 5 asignamos un int, que también es un object, a un elemento del object[]. El fragmento de código compila, por lo que podemos deducir que el compilador está de acuerdo con la afirmación de que un string[] es un object[]. Al ejecutar este fragmento de código, ocurre una excepción en tiempo de ejecución en la línea 4, ¿por qué? Por que se está intentando asignar un int a un elemento de un string[], que si bien está declarado como object[], que no deja de ser un string[]. El código da un error en tiempo de ejecución, por lo que podemos decir que el CLR no comparte la afirmación de que un string[] es un object[]. ¿Quién tiene razón? No es tan fácil. Intuitivamente, un string[] debería ser un object[]. Y todo estaría bien, si los arrays fueran inmutables, pues en ese caso sólo puedo “obtener” objetos del array, nunca “poner”, y por lo tanto nunca hago algo que no sea correcto en tiempo de ejecución. ¿Donde tengo esta misma situación de poder “obtener” objectos de una colección y nunca “poner”? Con la interfaz IEnumerable (declarada aproximadamente como): 1: IEnumerable<T> 2: { 3: IEnumerator<T> GetEnumerator(); 4: } 5: IEnumerator<T> 6: {7: bool MoveNext(); 8: T Current { get; } 9: }
Toda colección que implemente IEnumerable (y todas las de la biblioteca de .NET Framework lo hacen), retorna un IEnumerator. Con ese IEnumerator, puedo iterar por la colección con MoveNext, y “obtener” los elementos con Current. ¿Un IEnumerable<string> es un IEnumerable<object>? Veamos: 1: IEnumerable<string> sl = new List<string>(); 2: IEnumerable<object> = ol; 3: ol = sl;
En forma análoga a nuestro primer fragmento de código, en la línea 3 asignamos un IEnumerable<string> a un IEnumerable<object>. Pero a diferencia del fragmento anterior, éste no compila. Podemos decir que en este caso tanto el compilador como el CLR están de acuerdo. ¿Por qué esto es así? Aunque intuitivamente esta asignación debería ser posible, y no hay posibilidades de que ocurra el error en tiempo de ejecución como con los arrays, los diseñadores de C# entendieron que era mejor evitar que esta asignación fuera posible. ¿Qué relación tiene todo esto con varianza? De acuerdo, ya es hora de hablar de varianza. Cuando se da una situación como la anterior, en la que un tipo como IEnumerable<string> debería poder comportarse como -ser asignable a- IEnumerable<object>, decimos que IEnumerable debería ser covariante respecto de string y object. Esto es algo que el compilador no puede decidir, pues depende del uso que el programador quiera hacer de los tipos de datos; el compilador asume el uso lo más amplio y flexible posible, y corta por lo sano: no compila la asignación de la que venimos hablando. Hasta ahora, no podíamos en C# declarar la covarianza, es decir, no podíamos decirle al compilador que como sólo íbamos a “obtener” objetos a través de IEnumerable, nos dejara asignar IEnumerable<string> a IEnumerable<object>. Ahora se puede, con una nueva palabra clave out, introducida en C# 4.0. Con esta nueva palabra clave, IEnumerable e IEnumerator quedan así: 1: IEnumerable<out T> 2: { 3: IEnumerator<T> GetEnumerator(); 4: }5: IEnumerator<out T> 6: {7: bool MoveNext(); 8: T Current { get; } 9: }El caso opuesto es cuando queremos decir al compilador que sólo vamos a “poner” objetos a través de una interfaz, como IComparer, y que nos deje asignar IComparer<object> a IComparer<string>; es decir, que IComparer sea contravariante respecto de object y de string. Esto se logra con la palabra clave in: 1: IComparer<in T> 2: {3: int Compare(T x, T, y); 4: }
En la CTP del año pasado (la única disponible al momento de escribir este artículo), las interfaces como IEnumerable, IComparer, etc., todavía no tienen out o in; por lo que debemos usar otro ejemplo para mostrar covarianza y contravarianza. Veámoslo a continuación: 1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: 6: namespace Variance 7: {8: class Animal { } 9: class Cat : Animal { } 10: 11: class Program 12: {13: delegate T ReturnInstance<T>(); 14: delegate void ReceiveInstance<T>(T a); 15: 16: static Cat CreateCat() 17: {18: return new Cat(); 19: } 20: 21: static void PrintAnimal(Animal a) 22: { 23: Console.WriteLine(a); 24: } 25: 26: static void Main(string[] args) 27: {28: // Covariance 29: ReturnInstance<Cat> createCat = CreateCat; 30: ReturnInstance<Animal> createAnimal = createCat; 31: 32: // Contraviance 33: ReceiveInstance<Animal> processAnimal = PrintAnimal; 34: ReceiveInstance<Cat> processCat = processAnimal; 35: } 36: } 37: }Les adelanto que tal como está el código no compila. Veamos por qué. La clase Cat es sucesora de Animal. El delegate ReturnInstance<T> sólo puede retornar instancias. Cuando uso Animal y Cat para definir ReturnInstance, intuitivamente, yo sé que debería poder asignar una instancia de ReturnInstance<Cat> a una variable ReturnInstance<Animal>, porque lo que quiera que haga con el Animal retornado (invocar métodos, consumir propiedades, asignar, etc.), estará bien si en realidad es un Cat. Debemos decir que ReturnInstance es covariante respecto de T, para lo cual debemos declararla como delegate T ReturnInstance<out T>(). ¿Suena complicado? Veamos. Dadas las asignaciones 1: ReturnInstance<Cat> createCat = CreateCat; 2: ReturnInstance<Animal> createAnimal = createCat;la sentencia 1: Animal animal = createAnimal();equivale en realidad a 1: Animal animal = CreateCat();que es intuitivamente válida, pero que el compilador no la acepta a menos que declaremos delegate T ReturnInstance<out T>. Por su lado, el delegate void ReceiveInstance<T>(T) sólo puede recibir instancias. Cuando uso Animal y Cat para definir ReceiveInstance, intuitivamente yo sé que debería poder asignar una instancia de ReceiveInstance<Animal> a una variable ReceiveInstance<Cat>, porque cuando vaya a usar ReceiveInstance<Cat> tendré que pasar un Cat, que será recibido en donde se espera un Animal, lo cual no presenta ningún inconveniente. Debemos decir que ReceiveInstance es contravariante respecto de T, para lo cual debemos declararla como delegate void ReceiveInstance<in T>(T a). De nuevo, ¿es complicado? Vamos por partes. Dadas las asignaciones 1: ReceiveInstance<Animal> processAnimal = PrintAnimal; 2: ReceiveInstance<Cat> processCat = processAnimal;la sentencia 1: processCat(new Cat()); es equivalente a 1: PrintAnimal(new Cat()); que nuevamente es intuitivamente válida, pero que tampoco el compilar acepta a menos que declaremos delegate void ReceiveInstance<in T>(T a). Espero que haya quedado claro. Si así fue, pueden seguir leyendo las definiciones más formales que vienen a continuación; de lo contrario, les recomiendo que comiencen de nuevo desde el principio, o me hagan llegar comentarios para ver qué parte no se entendió. Aquí vamos. Covarianza y contravarianza son conceptos que se aplican a los tipos en un lenguaje de programación, orientado a objetos o no. La situación se presenta generalmente cuando intervienen jerarquías de tipos paralelas, como en los ejemplos anteriores. Formalmente, una operación se dice covariante si preserva el orden de los tipos, es decir, si aplicada a cualesquiera dos tipos S y T siempre resulta en dos tipos S’ y T’ con la misma relación (el mismo orden) que S y T. En programación orientada a objetos, el orden entre los tipos suele ser la relación subtipo-supertipo. Una operación se dice contravariante si invierte la relación, es decir, si aplicada a cualesquiera dos tipos S y T siempre resulta en dos tipos S’ y T’ con la relación opuesta (orden inverno) que S y T. La varianza aparece relacionado con el principio de sustitución de Liskov, que se enuncia asi: si para cada objeto O de tipo S existe un objeto O' de tipo T tal que para todos los programas P definidos en términos de T, el comportamiento de P permanece sin cambios cuando O es substituido por O', entonces S es un subtipo de T. Para que el comportamiento no cambie y O pueda ser sustitudo por O’, todas las operaciones deben ser covariantes en el resultado y contravariantes en los argumentos. En C# 4.0 la varianza es aplicable a las interfaces y los delegates. Espero que les haya servido. Hasta la próxima. トラックバックこの記事のトラックバックの URL は次のとおりです。 http://fernandomachadopiriz.spaces.live.com/blog/cns!8D961B36ABFD7A76!2615.trak この記事を参照しているブログ
|
|
|