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.

2 thoughts on “Struktury danych a interfejsy”

  1. W poście jest mały błąd:
    Najpierw jest przedstawiona deklaracja interfejsu i struktury:

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

    Wygenerowany IL to: (….)
    Niestety ten IL odnosi się do późniejszej operacji, a nie do powyższej

Leave a Reply

Your email address will not be published.