\


 Friday, 24 April 2009
Drobná poznámka ke kontravariancí delegátů v C#

Předpokládám, že se stejně jako já těšíte na lepší podporu kovariance a kontravariance u rozhraní a delegátů v připravované verzi  C# 4.0. Už dnes se ale dá s existující podporou kovariance a kontravariance u delegátů pěkně kouzlit – pro ty s exaktnějším přístupem ke kódu a vytříbenou terminologií se slovo “kouzlit” v knihách zásadně překládá jako “psát elegantnější kód”. Opakovat základy kovariance a kontravariance u delegátů zde nebudu a všechny ty, kteří sem zabloudili při svém ahashverovském “googlování” nějakého problému volně souvisejícího s probíraným tématem, odkážu na článek v MSDN. :-)

Kovarianci i kontravarianci delegátů používám rád, ale dnes se mi podařilo narazit na potíž, o které si nejsem jistý, že je všeobecně známa. Alespoň já jsem se po chvíli údivu a narůstajícího rozčilení nad tím, že můj dokonalý kód :-) nechce vybíravý kompilátor přijmout a neustále protestuje, musel zbaběle uchýlit ke specifikaci C# 3.0.

 

Takže zde je popis “problému”.

Tento kód asi nikoho nepřekvapí

 public delegate void MyAction<T>(T t);

    
    public class Base
    {
        
    }
    public class Derived : Base
    {
        
    }
    class Program
    {
        static void Main(string[] args)
        {

            MyAction<Derived> del = Test;
            
        }


        public static void Test(Base p)
        {
            Console.WriteLine(p);
            
    }
}

Máme generického delegáta MyAction, jehož instanci s názvem del vytvoříme v metodě Main. Za generický parametr T dosadíme generický argument typu “Derived” , přičemž delegát ukazuje na metodu Test, která přijímá argument Typu Base. Kontravariance zajistí, že tento kód bez problémů projde.

Zkusme udělat mírnou úpravu. Místo metody Test přiřadíme do delegáta del lambda výraz, u kterého explicitně určíme typ argumentu.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ContravarianceTest
{
    public delegate void MyAction<T>(T t);

    
    public class Base
    {
        
    }
    public class Derived : Base
    {
        
    }
    class Program
    {
        static void Main(string[] args)
        {

            MyAction<Derived> a = (Base b) => Console.WriteLine(b) ;
                          
            
        }

    }

}

Kompilátor se tentokrát razantně ohradí proti podobné manipulaci.

Cannot convert lambda expression to delegate type 'ContravarianceTest.MyAction<ContravarianceTest.Derived>' because the parameter types do not match the delegate parameter types    C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs   

A v další chybě své výhrady upřesní.

Parameter '1' is declared as type 'ContravarianceTest.Base' but should be 'ContravarianceTest.Derived'    C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs   

Shrneme-li to, je zřejmé, že při použití lambda výrazu kompilátor na nějakou kontravarianci argumentů zapomene a vyžaduje, aby typ argumentu v lambda výrazu byl identický s typem argumentu u delegáta. Ještě podotknu, že stejné chování s projeví i u negenerického delegáta - public delegate void MyAction(Derived  d);.

Použijeme-li anonymní metodu, kompilátor stále protestuje jako u lambda výrazů.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ContravarianceTest
{
    public delegate void MyAction<T>(T t);

    
    public class Base
    {
        
    }
    public class Derived : Base
    {
        
    }
    class Program
    {
        static void Main(string[] args)
        {
            MyAction<Derived> a = delegate(Base b)
                                      {
                                          Console.WriteLine(b);
                                      };
              

            
        }


    }

}

Cannot convert anonymous method to delegate type 'ContravarianceTest.MyAction<ContravarianceTest.Derived>' because the parameter types do not match the delegate parameter types    C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs    27    35

Parameter '1' is declared as type 'ContravarianceTest.Base' but should be 'ContravarianceTest.Derived'    C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs    27    44

U anonymních metod a lambda výrazů tedy kontravarianci nehledejme. Ve specifikacci C# 3.0 (C# 3.0 specification – sekce 7.14.1) nalezneme popis omezení pro anonymní metody a lambda výrazy.

“If an anonymous function has an explicit-anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have the same parameter types and modifiers in the same order. In contrast to method group conversions (§6.6), contra-variance of anonymous function parameter types is not supported. [zvýraznil R.S.] If an anonymous function does not have an anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have no out parameters.”

Většina programátorů v C# asi nikdy na podobné omezení nenarazí, a když už ano, vezmou jako fakt, “že to asi z nějakého důvodu nejde”. Mně přijde podobné chování neintutivní a vsadil bych se, že vetšina vývojářů, ať už vědomě či podvědomě, na C# oceňuje, že jde o jazyk, ve kterém na ně nečíhá mnoho záludností nebo nepříjemných překvapení. S přidáváním dalších a dalších rysů do jazyka vzrůstá pravděpodobnost, že některá nová vlastnost začne ovlivňovat způsob použití starší vlastnosti v jazyce a také vzrůstá počet rozdílů mezi konstrukcemi, které na první pohled vypadají stejně, nebo alespoň od nich poučenější vývojář, ačkoli si je vědom některých rozdílů například mezi delegátem ukazujícím na tradiční funkci, anonymní metodu, lamda výraz a složenou lambdu (lambda statement), očekává podobné chování.

Celou poznámku bychom mohli uzavřít dotazem: “Kolik nových vlastností programovací jazyk snese bez šrámů a posléze pořádných zářezů  na pověsti “jednoduchého” jazyka? Tipnul bych si, že empiricky si to budeme moci ověřit, až se začnou na fórech množit zoufalí vývojáři naříkající nad složitostí jazyka, jako se to děje dnes, když nějaká lama poté, co zbastlila při svých hrátkách “skorofunkčnípůlaplikaci” ve VB, Javě či C#, je nucena programovat v C++”? :-) Jde samozřejmě o hyperbolu, vždyť vím, že budoucnost na krásném IT úhoru je otevřena právě pro všechny ty pilné dělníky nové éry, kteří ochotně vygooglují různé nesouvislé fragmenty kódu, jež považují za společný komunitní majetek k instantnímu užití, dále zkombinují několik z nebe spadlých frameworků dohromady a jsou patřičně hrdi na to, že po dlouhé  praxi znají alespoň přibližný význam poloviny klíčových slov v C# či Javě.  Tedy pro ty, kterým třeba kontravariance v programovacím jazyce nikdy chybět nebude. :-)



Friday, 24 April 2009 11:57:02 (Central Europe Standard Time, UTC+01:00)       
Comments [6]  .NET Framework | Compact .Net Framework


Friday, 24 April 2009 12:48:01 (Central Europe Standard Time, UTC+01:00)
S kovariancí delegátů jsem měl před časem taky nějaký problém. Tuším, že delegát měl vracet object, ale nemohl jsem použít metodu, která vracela hodnotový typ...nebo tak nějak...
Friday, 24 April 2009 12:59:04 (Central Europe Standard Time, UTC+01:00)
Augi: To zní pravděpodobně. Kovariance s "ValueType" objekty také nefunguje, jak by dle dokumentace s mnoha výpustky čekal. Myslím, že hlavní problém je ten, že "ValueType" není brán jako potomek třídy Object, i když dokumentace se to snaží tvrdit.
Saturday, 25 April 2009 10:09:13 (Central Europe Standard Time, UTC+01:00)
Ano ano, to je přesně ono. Tehdy mě to zjištění docela nepotěšilo...
Tuesday, 05 May 2009 22:30:42 (Central Europe Standard Time, UTC+01:00)
Hmm, tak to je zajimave poznani (jasny, je to ve specifikaci, ale ...).
Thursday, 21 May 2009 01:15:33 (Central Europe Standard Time, UTC+01:00)
To s temi hodnotovymi typy je docela pochopitelne, protoze pokud se pouziva hodnotovy typ, tak se parameter pri volani delegatu ktery misto "int" ocekava "object" musi zaboxovat, coz by runtime pravdepodobne nemohl snadno poznat.

Jinak s delegaty je nestesti ze existuje vice typu ktere reprezentuji stejnou signaturu. V idealnim .NET frameworku by byly pouze Action<...> a Func<...> coz by hodne problemu zjendodusilo. Neni mi uplne jasne proc zrovna tohle nefunguje (nezda se mi ze by tam byl nejaky vazny problem). V C# 4.0 (s pouzitim modifikatoru "in") se ale kazdopadne da napsat toto:

MyAction<Derived> del2 = (MyAction<Base>)((Base d) => Console.WriteLine());

Nebo jeste lepe, da se nadefinovat (lehce absurdne vypadajici) pomocna metoda:

static MyAction<T> A<T>(MyAction<T> a) { return a; }

MyAction<Derived> del3 = A((Base d) => Console.WriteLine());

Mimochodem, myslim ze C# uz je za hranici toho kdy nejbeznejsi programatori jsou schopny pochopit co to vlastne dela. Otazka je do jaky miry to vadi - jen je potreba nejak zajistit aby pouzivali pouze "bezpecenou podmnozinu" ktere rozumeji a nektere pokrocile veci proste nechali nekomu jinemu :-).

PS: Uz jsem zpet v Praze tak se Ti ozvu! Nejak jsem zatim nic nestihal...
Thursday, 21 May 2009 11:05:50 (Central Europe Standard Time, UTC+01:00)
Tomáš Petříček:

U hodnotových typů se sice dá pochopit, že "boxing" celou záležitost komplikuje, ale podle mě je na vině hlavně (neúplná) dokumentace, která se snaží tvrdit, že "ValueType" je sice speciální, ale přesto stále potomek třídy Object, pro kterého platí některá omezení typu "z hodnotového typu nelze dědit". A to je dost zavádějícící "simplifikace". :)
Pro vývojáře je kovariance a kontravariance hodntových typů podobně matoucí jako kód.

short a = 1;
object obj = a;
int c = (int) obj; //Chyba, nelze "unboxovat" rovnou na int, i když existuje implicitní konverze ze short na int.

Pochopitelné se znalostí mechanismu "boxingu", ale matoucí pro "intuitivní" vývoj.

Ano, řešení fungují v C# 4.0. Pro C# 3.0 jsem zkoušel napsat (identickou s tvou metodou A) metodu Convert a úspěch se nedostavil.
class Adapters
{
public static MyAction<TB> Convert<TB>(MyAction<TB> action)
{
return action;
}
}


Ad PS) Já jsem na tom podobně, takže se nic nestalo - jen jsem zaregistroval, že ses vracel v dubnu. Ještě jsem se ani nedostal k opakovanému přečtení tvé knihy, protože se na mě navalila nutnost nastudovat a napsat věci ze zcela jiného soudku, takže F# si prozatím schovávám na romantické chvilky o dovolené. :) Ale na setkání se těším. :)
Comments are closed.