Interfejsy: implementacja jawna vs. niejawna

W C# można implementować interfejsy na dwa sposoby: jawny oraz niejawny. Rozważmy poniższy interfejs:

interface ISerializable
{
    void Serialize(string path);
}

Implementacja jawna:

class ExplicitImplementation:ISerializable
{

    #region ISerializable Members

    void ISerializable.Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Implementacja niejawna:

class ImplicitImplementation:ISerializable
{
    #region ISerializable Members

    public void Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Spotkałem się z dwoma przeciwstawnymi opiniami. Według jednej należy unikać implementacji jawnej a według drugiej powinna ona być traktowana jako domyślna i tylko w specyficznych sytuacjach powinno korzystać się  z niejawnej.

W poście chciałbym przedstawić za i przeciw dwóch podejść. W końcu skoro c# dostarcza dwa sposoby muszą istnieć jakieś zalety i wady obu rozwiązań.  Podejście jawne przede wszystkim jest niezbędne gdy klasa musi zaimplementować dwa interfejsy, które posiadają metody o takich samych sygnaturach a implementacje wyglądają inaczej. Przykład:

interface IXmlSerializable
{
    void Serialize(string path);
}
interface IBinarySerializable
{
    void Serialize(string path);
}

class ExplicitImplementation:IXmlSerializable,IBinarySerializable
{
    void IXmlSerializable.Serialize(string path)
    {        
    }

    void IBinarySerializable.Serialize(string path)
    {        
    }
}

Nie ma możliwości dostarczenia dwóch różnych implementacji jeśli wybierze się podejście niejawne. Pod tym względem, implementacja jawna niewątpliwie wygrywa z niejawną.

Kolejnym argumentem za jawną implementacją jest fakt, że nie można takich metod wywoływać inaczej niż z poziomu interfejsu. Przykład:

var explicitImplementation=new ExplicitImplementation();
explicitImplementation.Serialize();// blad
IXmlSerializable serializable = explicitImplementation;
serializable.Serialize(); // OK

Aby wywołać metodę zaimplementowaną w sposób jawny, zawsze trzeba najpierw ją zrzutować na interfejs. Ma to jednak jedną zaletę. Załóżmy, że pierwotna wersja interfejsu ma dwie metody:

interface IBinarySerializable
{
    void Serialize(string path);
    void Deserialize(string path);
}
class ImplicitImplementation:IBinarySerializable
{
    public void Serialize(string path)
    {
        throw new NotImplementedException();
    }
    public void Deserialize(string path)
    {
        throw new NotImplementedException();
    }
}

Po jakimś czasie jedna z metod interfejsu została usunięta (w tym przypadku akurat to nie ma sensu…) ponieważ nigdy nie była używana i interfejs wygląda następująco:

interface IBinarySerializable
{
    void Serialize(string path);
}

Co się dzieje? Kod nadal kompiluje się  mimo, że wciąż wszystkie klasy zawierają implementacje Deserialize. Gdyby wszystkie klasy implementowały Deserialize w sposób jawny, wtedy usunięcie metody z interfejsu wiązałoby się z koniecznością usunięcia wszystkich implementacji – nie pozostałby nigdzie martwy kod. Nie można po prostu skompilować kodu, gdzie metoda jest implementowana w sposób jawny a nie istnieje ona w interfejsie. Usuwanie metod jest oczywiście brzydkie (niezgodne z open\closed principle) ale czasami nie ma innego wyjścia.

Kolejną zaletą wymogu wywoływania przez interfejs jest promowanie izolacji interfejsu od implementacji. Na przykład gdyby metody były zaimplementowane w sposób niejawny wtedy często popełnianym błędem jest:

var serializer = new ImplicitImplementation();

W tym przypadku var jest typu ImplicitImplementation. Zgodnie z dobrymi praktykami powinno używać się czegoś bardziej abstrakcyjnego np. interfejsu. Z tego względu dużo lepiej jest:

IXmlSerializable serializator = ...

Należy operować na interfejsie a specyficzna implementacja powinna zostać wstrzyknięta np. przez konstruktor. Jawna implementacja interfejsu promuje takie podejście ponieważ nie da się tych metod wywołać bezpośrednio z klasy.

Niestety jawna implementacja ma również wady. Najważniejsza z nich to boxing, który ma miejsce dla value type. W poprzednim poście pisałem, że struktury również mogą implementować interfejsy. Pokazałem wtedy jedynie niejawne podejście jednak “nic” nie stoi na przeszkodzie aby wykonać to w sposób jawny:

struct SampleStruct:IBinarySerializable
{
    #region IBinarySerializable Members

    void IBinarySerializable.Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}

W takiej sytuacji jedyny sposób na wykonanie metody Serialzie to zrzutowanie struktury na interfejs, co powoduje oczywiście boxing:

IBinarySerializable binarySerializable = new SampleStruct();
binarySerializable.Serialize(path);

Inny argument przeciwko to fakt, że nie jest to zbyt intuicyjne. W końcu skoro klasa implementuje interfejs to naturalne jest, że oczekuje się od niej, że jest w stanie wykonać logikę opisaną przez interfejs. Bez dobrej dokumentacji użytkownikom ciężko będzie korzystać z takiej funkcjonalności. Niektóre typy  w .NET Framework potwierdzają tą regułę. Na przykład, jak skonwertować integer na char? Czy poniższy kod zadziała?

int integer = 5;
char character = integer.ToChar(null);

Niestety nie, ponieważ Int32 implementuje ToChar w sposób jawny i należy najpierw zrzutować obiekt na IConvertible:

int integer = 5;
char character = ((IConvertible) integer).ToChar(null);

Trzeba przyznać, że jest to naprawdę mało intuicyjne i na dodatek powoduje niechciany i niepotrzebny boxing. Kolejnym argumentem przeciw są utrudnienia w wykorzystywaniu interfejsu w klasach pochodnych tj.:

class ExplicitImplementation:IBinarySerializable
{
    #region IBinarySerializable Members

    void IBinarySerializable.Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}
class DerivedClass:ExplicitImplementation
{
    public void WrapSerialize()
    {
        IBinarySerializable baseClass = this;
        baseClass.Serialize(null);
    }
}

Prawda, że  tworzenie nowego wskaźnika aby wykonać metodę bazową, to trochę za wiele? Osobiście używam jawnych interfejsów w sytuacjach gdy dane metody są wykorzystywane tylko w jednym kontekście. Mam na myśli sytuacje, w której interfejs jest zaimplementowany tylko po to aby dostosować klasę do jakiegoś innego API– innymi słowy nie dodaje on nowej logiki a tylko eksponuje aktualną w inny sposób. W sytuacjach gdy interfejs to część opisywanej logiki wtedy preferuje niejawne podejście.

Struktury danych a interfejsy

O strukturach na blogu pisałem już wielokrotnie m.in.: “Klasy i struktury w C#”, “Dlaczego struktury nie mogą posiadać konstruktora bez parametrów?”, “StructLayout – wprowadzenie”, “StructLayout–zastosowanie”. W pierwszych z tych postów, przedstawiającym różnice między klasami a strukturami napisałem, że co prawda struktury nie mogą dziedziczyć po klasach ale mogą za to implementować interfejsy. Dzisiaj chciałbym rozszerzyć to o kilka słów gdyż w tamtym wpisie ograniczyłem się tylko do stwierdzenia, że jest to możliwe.

Na początek przykład:

interface IPerson
{
    string Name { get; set; }
}
struct Person:IPerson
{
    public string Name { get; set; }
}

Kod skompiluje się chociaż nie jest to popularna konstrukcja i całe szczęście… Zdecydowanie odradzam łączenia interfejsów ze strukturami. Dlaczego? W praktyce może to przynieść więcej problemów niż korzyści. Struktury należą do Value Type a nie typów referencyjnych. Przeznaczone są do prostych zadań typu przychowanie współrzędnych. Logika powinna być modelowania za pomocą klas i programowania obiektowego. Interfejs z kolei to typ referencyjny, esencja polimorfizmu. Mieszanie typów referencyjnych z typami value może przynieść nieoczekiwane efekty uboczne. Na przykład:

interface IPerson
{
    string Name { get; set; }
}
struct Person:IPerson
{
    public string Name { get; set; }
}

Wygenerowany IL to:

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] class IPerson person,
        [1] valuetype Person CS$0$0000)
    L_0000: nop 
    L_0001: ldloca.s CS$0$0000
    L_0003: initobj Person
    L_0009: ldloc.1 
    L_000a: box Person
    L_000f: stloc.0 
    L_0010: ldloc.0 
    L_0011: ldstr "test"
    L_0016: callvirt instance void IPerson::set_Name(string)
    L_001b: nop 
    L_001c: ret 
}
 

Niestety, w IL można zauważyć niechcianą instrukcję “box”, o której pisałem już tutaj. Nie powinno to stanowić niespodzianki ponieważ przypisanie value type do typu referencyjnego powoduje boxing.  Po co zatem używać interfejsów skoro powodują one boxing? Jeśli faktycznie to byłoby potrzebne wtedy lepiej już użyć od razu klas.

Najgorsza chyba sytuacja ma miejsce w takim przypadku:

private static void Main(string[] args)
{
   Person person=new Person();
   IPerson personInterface = person;
   personInterface.Name="test";
   Console.WriteLine(person.Name);
}

Jeśli programista nie wie, że Person jest strukturą a nie klasą wtedy spodziewa się, że na ekranie wyświetli się napis “test”. Niestety ze względu na boxing, obiekt zostanie przekopiowany w nowe miejsce i na ekranie nic nie wyświetli się  (Name jest pusty). Czytanie takiego kodu jest  oczywiście bardzo trudne i łatwo popełnić błędy. Interfejsy zostały wprowadzone po to aby móc operować na danych w sposób maksymalnie generyczny. Dla struktur nie ma to po prostu sensu zwłaszcza, że mieszanie reference type z value type powoduje powyższe niespodzianki.

Teraz pokażę, jak łagodzić powyższe skutki. Czasami jednak implementacja interfejsów ma sens. Dobrym przykładem jest IComparable, który dostarcza metodę CompareTo. Struktury powinny być wykorzystywane jako kontenery danych. Kontenery oczywiście często trzeba porównać stąd implementacja IComparable jest rozsądna. Innym, często implementowanym interfejsem jest IFormattable dostarczającym ToString. Należy jednak zawsze preferować interfejsy generyczne tzn.:

internal struct CustomType : IComparable
{
    public int CompareTo(object obj)
    {
        CustomType customType = (CustomType) obj;
        // jakas logika
        return 1;
    }
}
internal struct CustomType : IComparable<CustomType>
{
    public int CompareTo(CustomType other)
    {
        //..
        return 1;
    }
}

Pierwsza implementacja zawsze będzie powodować boxing np.:

CustomType customType1=new CustomType();
CustomType customType2=new CustomType();
customType1.CompareTo(customType2);

Wartość customType będzie musiała być zamieniona na typ referencyjny object. W przypadku generycznego interfejsu nie zachodzi taka potrzeba ponieważ wtedy CompareTo przyjmuje jako parametr wejściowy CustomType a nie object.

Inicjalizacja obiektów z kolekcjami.

O dwóch sposobach inicjalizacji właściwości obiektu pisałem już tutaj. Dziś chciałbym pokazać jak zainicjalizować kolekcję używając “nowego” mechanizmu wprowadzonego w C# 3.0. Z poprzedniego post’a wiadomo, że właściwości można ustawiać tak:

internal class Person
{
    public Person()
    {
    }
   public List<string> Collection { get; set; }
   public string Name { get; set; }
}

internal class Program
{
    private static void Main(string[] args)
    {
        var person = new Person {Name = "Piotr", Collection = new List<string>()};
    }
}

Istnieje jednak dodatkowy operator, który chyba nie jest tak bardzo oczywisty. Jeśli chcielibyśmy dodać elementy do kolekcji możemy:

internal class Person
{
    public Person()
    {
        Collection=new List<string>();
    }
   public List<string> Collection { get; set; }
   public string Name { get; set; }
}

internal class Program
{
    private static void Main(string[] args)
    {
        var person = new Person {Name = "Piotr", Collection = {"A", "B", "C"}};
        foreach (var item in person.Collection)
            Console.WriteLine(item);
    }
}

CLR jest na tyle inteligentny, że jeśli kolekcja implementuje interfejs ICollection (posiada metodę Add) wtedy konstrukcja w postaci {lista_elementów} dodaje elementy a nie nadpisuje. Aby potwierdzić działanie dodajmy część elementów już w konstruktorze:

internal class Person
{
    public Person()
    {
        Collection=new List<string>();
        Collection.Add("F");
    }
   public List<string> Collection { get; set; }
   public string Name { get; set; }
}

Następnie uruchamiając powyższą inicjalizację na ekranie wyświetli się F,A,B,C a nie A,B,C  – elementy zostały dodane a nie nadpisane. Konstrukcja jest podobna ale różni się jednak trochę od inicjalizatora tablic, który wygląda następująco:

internal class Person
{
    public Person()
    {
    }
   public string[] Items { get; set; }
   public string Name { get; set; }
}

internal class Program
{
    private static void Main(string[] args)
    {
        var person = new Person {Name = "Piotr", Items = new[] {"A", "B", "C"}};
        foreach (var item in person.Items)
            Console.WriteLine(item);
    }
}

Powyższa konstrukcja to zwykłe utworzenie tablicy zatem elementy zostaną nadpisane (jeśli jakieś istniały przedtem). Oczywiście inicjalizacja kolekcji nie jest ograniczona tylko do powyższych przykładów. Każda lista (i inne klasy implementujące ICollection) można inicjalizować następująco:

internal class Program
{
    private static void Main(string[] args)
    {
        List<string> items = new List<string>() {"A", "B", "C"};
    }
}

Analogicznie wygląda sytuacja ze słownikami:

Dictionary<string, string> items = new Dictionary<string, string>()
                                               {{"klucz", "wartość"}, {"klucz2", "wartosc2"}};

Code Review: implementacja wewnętrzna wyrażeń lambda oraz metod anonimowych

Wyrażenia lambda są łatwe w użyciu, ale jak to bywa z takimi ułatwieniami również nieświadomie można spowodować poważne problemy. Przykład:

class SampleClass
{    
}
class Factory
{
    private Type _type = typeof (SampleClass);
    
    public Func<SampleClass> Create()
    {
        return () => (SampleClass)Activator.CreateInstance(_type);
    }
}

internal class Program
{
   private static void Main(string[] args)
   {
       Task task=Task.Factory.StartNew(Run);
       task.Wait();
   }
    private static Func<SampleClass> Create()
    {
        Factory factory=new Factory();
        return factory.Create();
    }

   private static void Run()
   {
       Func<SampleClass> factory = Create();
       while (true)
       {
           // some logic
       }
   }
}

Powyższy kod spowoduje memory leak. Dlaczego? Wyrażenie lambda potrzebuje dostęp do pola klasy Factory co skutkuje trzymaniem w pamięci całego obiektu Factory. Może być to trochę mylące bo w końcu w metodzie Program:Create, referencja Factory znajduje się po za scope i wydawałoby się, że GC usunie obiekt z pamięci.

Aby w pełni to zrozumieć warto przeanalizować z Reflector zasadę działania lambda oraz metod anonimowych. Pierwsza możliwość to przypadek gdy lambda potrzebuje dostęp do pola klasy:

class Factory
{
    private Type _type = typeof (SampleClass);
    
    public Func<SampleClass> Create()
    {
        return () => (SampleClass)Activator.CreateInstance(_type);
    }
}

CLR musi jakoś przedłużyć czas życia obiektowi _type. Normalnie _type zostałby zwolniony gdy nie ma referencji do Factory. Zaglądając do wygenerowanego kodu, można dowiedzieć się, że została stworzona nowa metoda w klasie Factory. Podstawowa implementacja Create wygląda następująco:

.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
{
    .maxstack 3
    .locals init (
        [0] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldftn instance class SampleClass Factory::<Create>b__0()
    L_0008: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int)
    L_000d: stloc.0 
    L_000e: br.s L_0010
    L_0010: ldloc.0 
    L_0011: ret 
}

Jak widać, zamiast lambdy, zwracana jest nowo wygenerowana metoda o nazwie <Create>b__0():

.method private hidebysig instance class SampleClass <Create>b__0() cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .maxstack 1
    .locals init (
        [0] class SampleClass CS$1$0000)
    L_0000: ldarg.0 
    L_0001: ldfld class [mscorlib]System.Type Factory::_type
    L_0006: call object [mscorlib]System.Activator::CreateInstance(class [mscorlib]System.Type)
    L_000b: castclass SampleClass
    L_0010: stloc.0 
    L_0011: br.s L_0013
    L_0013: ldloc.0 
    L_0014: ret 
}

Powyższe przykłady wyjaśniają memory leak przedstawiony na początku artykułu. W praktyce, nowa metoda jest tworzona a następnie zwracany jest do niej wskaźnik. Dzieje się tak ponieważ lambda potrzebuje dostęp do prywatnego pola. Nowa utworzona metoda oczywiście taki dostęp będzie miała. Bardziej skomplikowanym przypadkiem jest dostęp do pól lokalnych np.:

class Factory
{    
    public Func<SampleClass> Create()
    {
        SampleClass sampleClass=new SampleClass();
        return () =>  sampleClass;
    }
}

Wygenerowanie nowej metody nie rozwiąże problemu – metody nie mają dostępu do zmiennych lokalnych innych metod. CLR wygeneruje nową klasę opakowująca, która jest zagnieżdżona w Factory:

class private auto ansi beforefieldinit Factory
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }
    .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
    {
    }
    .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
        .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
        {
        }

        .method public hidebysig instance class SampleClass <Create>b__0() cil managed
        {
        }


        .field public class SampleClass sampleClass

    }
}

Nowa klasa DisplayClass1 posiada pole SampleClass oraz metodę Createb_0. Innymi słowy, lambda została przeniesiona do nowej klasy (DisplayClass1). Logika zawarta jest w Createb__0 a dane (SampleClass) w publicznym polu. Następnie w oryginalnej metodzie Factory:Create zamiast wywołania zwykłej metody należy stworzyć obiekt DisplayClass1 i przypisać SampleClass do pola publicznego:

.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
{
    .maxstack 3
    .locals init (
        [0] class Factory/<>c__DisplayClass1 CS$<>8__locals2,
        [1] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000)
    L_0000: newobj instance void Factory/<>c__DisplayClass1::.ctor()
    L_0005: stloc.0 
    L_0006: nop 
    L_0007: ldloc.0 
    L_0008: newobj instance void SampleClass::.ctor()
    L_000d: stfld class SampleClass Factory/<>c__DisplayClass1::sampleClass
    L_0012: ldloc.0 
    L_0013: ldftn instance class SampleClass Factory/<>c__DisplayClass1::<Create>b__0()
    L_0019: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int)
    L_001e: stloc.1 
    L_001f: br.s L_0021
    L_0021: ldloc.1 
    L_0022: ret 
}

Linia “L_000d: stfld class SampleClass Factory/<>c__DisplayClass1::sampleClass” przypisuje utworzoną instancję SampleClass do pola publicznego obiektu DisplayClass1. Reszta myślę, że powinna być jasna.

Co w przypadku dostępu do obiektów statycznych? Sprawdźmy:

class Factory
{    
    static SampleClass _sampleClass=new SampleClass();
    public Func<SampleClass> Create()
    {
        return () => _sampleClass;
    }
}

Zostanie wygenerowana po prostu nowa metoda ale statyczna:

.class private auto ansi beforefieldinit Factory
    extends [mscorlib]System.Object
{
    .method private hidebysig specialname rtspecialname static void .cctor() cil managed
    {
    }

    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }

    .method private hidebysig static class SampleClass <Create>b__0() cil managed
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    }

    .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
    {
    }


    .field private static class SampleClass _sampleClass

    .field private static class [mscorlib]System.Func`1<class SampleClass> CS$<>9__CachedAnonymousMethodDelegate1
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    }

}

Kolejna interesująca kwestia to przekazanie dwóch lokalnych zmiennych o różnym scope np.:

class Factory
{    
    public Func<SampleClass> Create()
    {
        int i = 0; // scope I

        if(i>=0)
        {
            int j = 0; // scope II
            return () => new SampleClass(i,j);
        }
        return null;
    }
}

CLR wygeneruje dwie klasy. Jedna będzie przechowywać zmienną “i” (scope I) druga z kolei będzie przechowywać “j”(scope II), metodę oraz referencję do wrapper’a pierwszego ( w formie pola publicznego):

.class private auto ansi beforefieldinit Factory
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }

    .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
    {
    }
    .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
        .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
        {
        }


        .field public int32 i

    }
    .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass3
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
        .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
        {
        }

        .method public hidebysig instance class SampleClass <Create>b__0() cil managed
        {
        }


        .field public class Factory/<>c__DisplayClass1 CS$<>8__locals2

        .field public int32 j

    }
}

Oraz przykład użycia wrapper’ow:

.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
{
    .maxstack 3
    .locals init (
        [0] class Factory/<>c__DisplayClass3 CS$<>8__locals4,
        [1] class Factory/<>c__DisplayClass1 CS$<>8__locals2,
        [2] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000,
        [3] bool CS$4$0001)
    L_0000: newobj instance void Factory/<>c__DisplayClass1::.ctor()
    L_0005: stloc.1 
    L_0006: nop 
    L_0007: ldloc.1 
    L_0008: ldc.i4.0 
    L_0009: stfld int32 Factory/<>c__DisplayClass1::i
    L_000e: ldloc.1 
    L_000f: ldfld int32 Factory/<>c__DisplayClass1::i
    L_0014: ldc.i4.0 
    L_0015: clt 
    L_0017: stloc.3 
    L_0018: ldloc.3 
    L_0019: brtrue.s L_003f
    L_001b: newobj instance void Factory/<>c__DisplayClass3::.ctor()
    L_0020: stloc.0 
    L_0021: ldloc.0 
    L_0022: ldloc.1 
    L_0023: stfld class Factory/<>c__DisplayClass1 Factory/<>c__DisplayClass3::CS$<>8__locals2
    L_0028: nop 
    L_0029: ldloc.0 
    L_002a: ldc.i4.0 
    L_002b: stfld int32 Factory/<>c__DisplayClass3::j
    L_0030: ldloc.0 
    L_0031: ldftn instance class SampleClass Factory/<>c__DisplayClass3::<Create>b__0()
    L_0037: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int)
    L_003c: stloc.2 
    L_003d: br.s L_0043
    L_003f: ldnull 
    L_0040: stloc.2 
    L_0041: br.s L_0043
    L_0043: ldloc.2 
    L_0044: ret 
}

Gdyby wszystkie zmienne miały ten sam poziom scope wtedy zostałby wygenerowany tylko jeden wrapper:

internal class Factory
{
    public Func<SampleClass> Create()
    {
        int i = 0; // scope I      
        int j = 0; // scope I
        return () => new SampleClass(i, j);
    }
}

.class private auto ansi beforefieldinit Factory
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }

    .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
    {
    }



    .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
        .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
        {
        }

        .method public hidebysig instance class SampleClass <Create>b__0() cil managed
        {
        }


        .field public int32 i

        .field public int32 j

    }
}

Ostatni przypadek jaki chciałbym omówić jest dostęp do pól zarówno lokalnych jak i klasy:

internal class Factory
{
    private readonly SampleClass _sampleClass = new SampleClass();

    public Func<SampleClass> Create()
    {
        int i = 0; // scope I      
        return delegate
                   {
                       _sampleClass.SetValue(i);// field
                       return _sampleClass;
                   };
    }
}

Zostanie wygenerowany wrapper – to oczywiste ponieważ wymagany jest dostęp do danych lokalnych. Następnie wrapper ma dostęp do klasy Factory, która z kolei posiada dostęp do pola _sampleClass:

.class private auto ansi beforefieldinit Factory
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }

    .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
    {
    }


    .field private initonly class SampleClass _sampleClass



    .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
        .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
        {
        }

        .method public hidebysig instance class SampleClass <Create>b__0() cil managed
        {
        }


        .field public class Factory <>4__this

        .field public int32 i

    }

Przed zwróceniem delegat’y należy zatem stworzyć instancję DisplayClass1 i ustawić pole “i”(zmienna lokalna) oraz Factory (dostęp do _sampleClass):

.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed
{
    .maxstack 3
    .locals init (
        [0] class Factory/<>c__DisplayClass1 CS$<>8__locals2,
        [1] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000)
    L_0000: newobj instance void Factory/<>c__DisplayClass1::.ctor()
    L_0005: stloc.0 
    L_0006: ldloc.0 
    L_0007: ldarg.0 
    L_0008: stfld class Factory Factory/<>c__DisplayClass1::<>4__this
    L_000d: nop 
    L_000e: ldloc.0 
    L_000f: ldc.i4.0 
    L_0010: stfld int32 Factory/<>c__DisplayClass1::i
    L_0015: ldloc.0 
    L_0016: ldftn instance class SampleClass Factory/<>c__DisplayClass1::<Create>b__0()
    L_001c: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int)
    L_0021: stloc.1 
    L_0022: br.s L_0024
    L_0024: ldloc.1 
    L_0025: ret 
}
 

Kiedyś pisałem już o niespodziankach związanych z lambda. Polecam post “Wyrażenia lambda i niespodziewany rezultat” , przestawiający generowanie DisplacClass dla pętli.

Wywoływanie zdarzeń za pomocą metod rozszerzających

O zdarzeniach było już wielokrotnie na blogu. Pokazywałem różne sposoby wywołania zdarzeń. Najpopularniejszym chyba sposobem jest poniższy wzorzec:

public class Person
{
   public event EventHandler FirstNameChanged;
   
   virtual protected void OnFirstNameChanged(EventArgs e)        
   {
       if (FirstNameChanged != null)
           FirstNameChanged(this, e);            
   }
}

Jeśli wielowątkowość wchodzi w grę wtedy lepiej napisać:

public class Person
{
   public event EventHandler FirstNameChanged;
   
   virtual protected void OnFirstNameChanged(EventArgs e)
   {
       EventHandler handler = FirstNameChanged;
       if (handler != null)
           handler(this, e);            
   }
}

Poprzednia instrukcja IF oczywiście była niebezpieczna z punktu widzenia wielowątkowości. W momencie wykonania pierwszego if’a referencja do FirstNameChanged mogłaby zostać ustawiona na NULL tym samym powodując NullReferenceException w momencie wywołania handler’a. Drugi fragment w praktyce działa ale z punktu widzenia wielowątkowości również nie jest bezpieczny. Po pierwsze kompilator mógłby to zoptymalizować i usunąć zmienną tymczasową handler (tym samym niczym by to się nie różniło od pierwszego przykładu). Drugim powodem jest caching, który może spowodować, że nie będzie brana wartość najnowsza a stara z bufora. Z tego co wiem, drugi przykład jest na tyle popularnym wzorcem, że kompilator C# wie o tym i nie będzie próbował zoptymalizować kodu a tym samym spowodować, że kod może zakończyć się NullReferenceException. Z tego względu powszechnie uznaje się, że kod jest bezpieczny. Jeśli jednak ktoś chce pisać kod, który z punktu matematycznego jest poprawny wtedy należy użyć np. Interlocked.Exchange:

public class Person
{
   public event EventHandler FirstNameChanged;

   protected virtual void OnFirstNameChanged(EventArgs e)
   {
       EventHandler handler = Interlocked.CompareExchange(ref FirstNameChanged, null, null);

       if (handler != null)
           handler(this, e);
   }
}

Trzeba przyznać, że jest to dość czasochłonne i nudne – za każdym razem trzeba pisać instrukcję IF. Z tego względu warto napisać metodę rozszerzająca dla EventArgs:

public static class EventArgsExtensions
{
   public static void Raise<TEventArgs>(this TEventArgs e,
                                        Object sender, ref EventHandler<TEventArgs> eventHandler)
       where TEventArgs : EventArgs
   {
       EventHandler<TEventArgs> handler = Interlocked.CompareExchange(ref eventHandler, null, null);
       if (handler != null) 
           handler(sender, e);
   }
}

Teraz w OnFirstNameChanged wystarczy:

public class Person
{
   public event EventHandler<EventArgs> FirstNameChanged;

   protected virtual void OnFirstNameChanged(EventArgs e)
   {
       e.Raise(this,ref FirstNameChanged);
   }
}

Innym ciekawym sposobem jest rozszerzenie samego handler’a”:

static public class EventExtensions
{
   static public void RaiseEvent(this EventHandler eventHandler, object sender, EventArgs e)
   {
       var handler = eventHandler;
       if (handler != null)
           handler(sender, e);
   }
   static public void RaiseEvent<T>(this EventHandler<T> eventHandler, object sender, T e)
       where T : EventArgs
   {
       var handler = eventHandler;
       if (handler != null)
           handler(sender, e);
   }
}

W takim przypadku wywołanie FirstNameChanged sprowadza się do:

public class Person
{
   public event EventHandler<EventArgs> FirstNameChanged;

   protected virtual void OnFirstNameChanged(EventArgs e)
   {
       FirstNameChanged.RaiseEvent(this,e);
   }
}

Optymalizacja klas z wieloma zdarzeniami – EventHandlerList

Zdarzenia stanowią bardzo wygodny mechanizm monitorowania stanu obiektów. W .NET można spotkać je na każdym kroku. Kontrolki zarówno w WinForms jak i WPF, posiadają wiele zdarzeń, często kilkadziesiąt. Niestety każda deklaracja zdarzenia pochłania zasoby. Nie ma to znaczenia gdy obiekt posiada tylko kilka zdarzeń ale może to być zauważalne dla skomplikowanych klas np. kontrolek w WinForms. Każda kontrolka eksponuje dziesiątki zdarzeń a użytkownicy zwykłe korzystają wyłącznie z kilku – rzadko ma miejsce sytuacja w której trzeba do wszystkich się podpiąć. Warto zobaczyć co naprawdę generuje prosta deklaracja zdarzenia:

internal class Program
{
    public event EventHandler SampleEvent;

    private static void Main(string[] args)
    {     
    }
}

Reflector:

.class private auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    .event [mscorlib]System.EventHandler SampleEvent
    {
        .addon instance void Program::add_SampleEvent(class [mscorlib]System.EventHandler)
        .removeon instance void Program::remove_SampleEvent(class [mscorlib]System.EventHandler)
    }


    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }

    .method private hidebysig static void Main(string[] args) cil managed
    {
        .entrypoint
    }


    .field private class [mscorlib]System.EventHandler SampleEvent

}

Jak wdać, została wygenerowana specjalna właściwość oraz delegata. Pytanie brzmi: po co generować tyle pól delegate, skoro tylko kilka zdarzeń zwykle jest wykorzystywanych?

Klasa EventHandlerList to po prostu kolekcja zdarzeń. Przykład:

class SampleClass
{
    protected EventHandlerList _events = new EventHandlerList();

    public event EventHandler SampleEvent1
    {
        add { _events.AddHandler("SampleEvent1", value); }
        remove { _events.RemoveHandler("SampleEvent1", value); }
    }
    public event EventHandler SampleEvent2
    {
        add { _events.AddHandler("SampleEvent2", value); }
        remove { _events.RemoveHandler("SampleEvent2", value); }
    }
    public void TestEvent1()
    {
        EventHandler eh = _events["SampleEvent1"] as EventHandler;
        if (eh != null)
        {
            eh(this, null);
        }
    }
}

EventHandlerList stanowi pojemnik na zdarzenia. Zamiast tworzyć za każdym razem delegate, w powyższym kodzie są one tworzone wyłącznie gdy zajdzie taka potrzeba. Klasa, która ma 50 zdarzeń zużyje pamięć wyłącznie na EventHandlerList i potrzebne zdarzenia. Wywołanie takiego zdarzenia jest proste ponieważ do EventHandler’a można dostać się jak do słownika – poprzez przekazanie klucza. Częstym wzorcem jest użycie klucza jako statyczne pole read-only:

internal class SampleClass
{
    protected EventHandlerList _events = new EventHandlerList();
    private static readonly object SampleEventKey = new object();

    public event EventHandler SampleEvent1
    {
        add { _events.AddHandler(SampleEventKey, value); }
        remove { _events.RemoveHandler(SampleEventKey, value); }
    }
    public void TestEvent1()
    {
        EventHandler eh = _events[SampleEventKey] as EventHandler;
        if (eh != null)
        {
            eh(this, null);
        }
    }
}

EventHandlerList jest powszechnie wykorzystywany w bibliotekach Microsoft’u. Na przykład każda kontrolka w WinForms ma właściwość Events, która jest typu EventHandlerList. Z tego względu poniższy kod można uznać za anty-wzorzec:

public class CustomButton : Button
{
   public event EventHandler SampleEvent;
}

Dużo lepiej jest napisać:

public class CustomButton : Button
{
    private static readonly object SampleEventKey = new object();
    
    public event EventHandler SampleEvent1
    {
        add { Events.AddHandler(SampleEventKey, value); }
        remove { Events.RemoveHandler(SampleEventKey, value); }
    }
    public void TestEvent1()
    {
        EventHandler eh = Events[SampleEventKey] as EventHandler;
        if (eh != null)
        {
            eh(this, null);
        }
    }
}

Code review: słowniki i podklasy

Kod:

sealed class FolderFilesMappings : Dictionary<string, IEnumerable<string>>
{
  // brak specyficznej implementacji czy rozszerzen
}

Powyższy kod przedstawia klasę, która jest wrapperem dla słownika. Rozwiązanie na pierwszy rzut oka wygląda ładnie ale osobiście zastanowiłbym się nad sensem pisania dodatkowej klasy, która tak naprawdę nic nie robi. Klasy powinny zawierać jakieś dane lub logikę. Powyższy fragment nie rozszerza funkcjonalności – wyłącznie daje opisową nazwę i skraca składnie – pisanie za każdym razem powyższej definicji słownika jest po prostu niewygodne.

Innym problemem jest fakt, że  poniższa metoda nie zaakceptuje czystego słownika jako parametr:

internal class Program
{
   private static void ProcessData(FolderFilesMappings mappings)
   {
       // logika tutaj
   }
   private static void Main(string[] args)
   {
        var data = new Dictionary<string, IEnumerable<string>>();
        ProcessData(data); // blad kompilacji
   }
}

W zasadzie FolderFilesMappings  to zwykły słownik dlatego rozsądne wydaje się, aby metody akceptowały również bazowy słownik jako parametr wejściowy.

Jeśli podklasa nie dodaje żadnego pola, zachowania  wtedy lepszym rozwiązaniem jest użycie klauzuli using. Ma to sens zwłaszcza, gdy klasa jest używana wyłącznie w jednym pliku – wtedy zupełnie nie ma sensu na pisanie kolejnej klasy. Przykład:

using System.Collections.Generic;
using FolderFilesMappings = System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>;

internal class Program
{
    private static void ProcessData(FolderFilesMappings mappings)
    {
        // logika tutaj
    }

    private static void Main(string[] args)
    {
        var data = new Dictionary<string, IEnumerable<string>>();
        ProcessData(data);

    }
}

Dzięki using można używać skróconej formy FolderFilesMapping ale jeśli to koniecznie, również można operować na czystym słowniku. Klauzula using to czysty skrót – na etapie kompilacji wszystkie wystąpienia FolderFilesMapping zostaną zastąpione definicją słownika. Nie ma sensu tworzenia nowej klasy tylko po to aby służyła ona jako zwykły skrót.

Code review: method extensions oraz call\callvirt

Zaczynamy od próbki kodu:

static class StringExtensions
{
    public static void SayHello(this string str, string message)
    {
        Console.WriteLine(string.Format("Hello:{0}", message));
    }
}
internal class Program
{
    private static void Main(string[] args)
    {
        string str = null;
        str.SayHello("Piotr");
    }
}

Co według Was wydarzy się po uruchomieniu programu? Na pierwszy rzut oka może wydawać się, że wystąpi NullReferenceException ponieważ wywołujemy metodę na nieistniejącym obiekcie.  Nie doświadczymy tego jednak a to ze względu na użycie instrukcji call a nie callvirt.

W IL istnieją dwa sposoby wykonania metod: call oraz callvirt. W zdecydowanej większości wykorzystywany jest callvirt, który może wywołać również metody wirtualne (co sprowadza się do przeszukiwania tablicy wskaźników). Warto podkreślić, że w C# nawet metody niewirtualne wywoływane są przez callvirt a nie przez call. Instrukcja call z kolei wykorzystywana jest przez wszystkie metody statyczne oraz value type (struktury danych). Ważną różnicą między call a callvirt jest fakt, że callvirt zawsze sprawdzi czy dana instancja jest różna od NULL. Ze względu, że powyższa klasa jest statyczna, to kompilator wyemitował instrukcję call, która nie sprawdza czy instancja jest NULL’em. Podobny kod, ale wywołujący zwykłą metodę, zakończyłby się wyjątkiem ponieważ w c# wszystkie inne metody typu instance na klasach wykorzystują callvirt ( a nie call).

Ktoś może zadać pytanie: dlaczego callvirt jest również wykorzystywany dla niewirtualnych metod? Odpowiedzią jest powyższy fragment kodu, który zachowuje się dziwnie. Gdy programista widzi wywołanie metody na obiekcie NULL myśli, że zakończy się to NullReferenceException. Team c# uznał, że lepiej użyć callvirt i sprawdzać za każdym razem czy instancja jest różna od NULL. Niestety nie wzięli pod uwagę rozszerzeń metod, które tak naprawdę są statycznymi klasami.

Nie wszystkie języki jednak emitują callvirt dla metod niewirtualnych. Niektóre korzystają z call i dlatego taki kod może w niektórych językach zadziałać:

internal class Program
{
    private void SayHello()
    {
        Console.WriteLine("Hello");
        
    }
    private static void Main(string[] args)
    {
        Program p = null;
        p.SayHello();
    }
}

W C# wywoła NullReferenceException ponieważ SayHello jest instance-method i wyemitowana zostanie instrukcja callvirt. Należy jednak zdawać sobie sprawę jakie ryzyko istnieje gdy piszemy biblioteki. Załóżmy, że mamy klasę z niewirtualną metodą w bibliotece A. Następnie biblioteka B wywołuję tą metodę. Wszystko na tym etapie działa. Ale po kilku miesiącach zmieniamy implementację w bibliotece A i metoda od teraz jest wirtualna. Tutaj pojawia się problem jeśli nie skompilujemy ponownie biblioteki B. Biblioteka B w końcu w niektórych językach mogła wyemitować po prostu call a nie callvirt. Z tego względu, bez rekompilacji, metoda będzie wywoływana zawsze niewirtualnie pomimo, że została już zmieniona na wirtualną. Oczywiście w c# taki problem nie istnieje ponieważ tam zawsze emitowany jest callvirt.

C#: ref vs. out

W C# typy proste przekazywane są przez wartość. Oznacza to, że za każdym razem wszystkie bity są kopiowane. Ponadto jakiekolwiek operacje dokonywane na takim polu, nie są widoczne na zewnątrz, na przykład:

internal class Program
{   
    private static void Increment( int value)
    {
        value++;
    }
    private static void Main(string[] args)
    {
        int value = 5;
        Increment(value);
        Console.WriteLine(value);
    }
}

Czasami zachodzi potrzeba przekazania wyniku z powrotem albo ze względu na dużą strukturę danych, nie chcemy kopiować wszystkiego za każdym razem. Służą do tego dwa operatory: ref oraz out. Wewnętrznie (w CLR) wyglądają praktycznie tak samo. Różnica jednak jest dla kompilatora i dla programisty. W skrócie Ref:

  1. Przekazuje zmienną przez adres, a nie przez wartość .
  2. Zmienna przekazana przez REF musi być już zainicjalizowana.

Z kolei out:

  1. Tak samo jak ref, również przekazuje zmienną przez adres.
  2. Zmienna przekazana przez OUT nie musi być zainicjalizowana (aczkolwiek może).
  3. Zmienna przekazana przez OUT musi za to być zainicjalizowana w ciele metody.

Punkt pierwszy jest chyba oczywisty ale spróbujmy napisać trochę kodu, który to udowodni:

// OUT
internal class Program
{   
    private static void SetToFive(out  int value)
    {
        value = 5;
    }
    private static void Main(string[] args)
    {
        int value = 0;
        SetToFive(out value);
        Console.WriteLine(value);
    }
}
// REF
internal class Program
{   
    private static void SetToFive(ref  int value)
    {
        value = 5;
    }
    private static void Main(string[] args)
    {
        int value = 0;
        SetToFive(ref value);
        Console.WriteLine(value);
    }
}

Obie powyższe funkcje wydrukują 5 – pierwszy punkt jest zatem prawdziwy. Następnie przekonajmy się o punkcie 2. Dla ref musimy zawsze zainicjalizować wartość przed przekazaniem jej do metody. Poniższy kod nie zadziała:

internal class Program
{   
    private static void SetToFive(ref  int value)
    {
        value = 5;
    }
    private static void Main(string[] args)
    {
        int value;
        SetToFive(ref value);
        Console.WriteLine(value);
    }
}

Przed wywołaniem SetToFive, value musi mieć już jakąś wartość. Poprawna wersja to:

private static void Main(string[] args)
{
   int value = 10;
   SetToFive(ref value);
   Console.WriteLine(value);
}

Z kolei dla out, nie trzeba inicjalizować value:

internal class Program
{
    private static void SetToFive(out  int value)
    {
        value = 5;
    }
    private static void Main(string[] args)
    {
        int value ;
        SetToFive(out value);
        Console.WriteLine(value);
    }
}

Pozostał punkt trzeci. Jeśli oznaczymy parametr out, wtedy w ciele metody musimy dokonać inicjalizacji a nie tylko modyfikacji. Na przykład poniższy kod nie zadziała:

internal class Program
{
    private static void Increment(out  int value)
    {
        value = value + 1;
    }
    private static void Main(string[] args)
    {
        int value = 10;
        Increment(out value);
        Console.WriteLine(value);
    }
}

OUT przyjmuje zawsze, że parametr nie jest zainicjalizowany a praca na takim obiekcie jest oczywiście niemożliwa.W powyższy przykładzie należy zatem użyć REF, który wymaga aby wartość przekazana do funkcji była już zainicjalizowana. W przypadku OUT, to metoda odpowiada za tą operację. Nie możemy nic zrobić z parametrem OUT jeśli najpierw nie przypiszemy do niej jakieś wartości. Jeśli zatem piszemy jakąś metodę, która zwraca kilka wyników, wtedy lepiej oznaczyć je jako OUT, który wymusi na nas wypełnienie ich. Z Kolei REF lepiej nadaję się do przypadków gdy po prostu nie chcemy przekazywać przez wartość (wydajność) lub chcemy jakąś zmienną jedynie zmodyfikować.

Na zakończenie jeszcze pokaże, że można również korzystać z named parameters i powyższych słów kluczowych:

internal class Program
{
    private static void SetToFive(out  int value)
    {
        value = 5;
    }
    private static void Main(string[] args)
    {
        int x = 5;
        SetToFive(value:out x);        
    }
}

Jak widać, składnia trochę może dziwna, ale jest to jak najbardziej możliwe.