Dlaczego struktury nie mogą posiadać konstruktora bez parametrów?

Na początku stwórzmy strukturę z konstruktorem przyjmującym dwa parametry:

public struct SampleStruct
{
    public SampleStruct(int x, int y)
    {
        _x = x;
        _y = y;
    }
    private int _x, _y;
}

Czy powyższy kod jest prawidłowy? Tak, skompiluje się i będzie działał bez problemów. Zdefiniujmy więc domyślny konstruktor:

public struct SampleStruct
{
   public SampleStruct()
   {
       _x = _y = 0;
   }
   public SampleStruct(int x, int y)
   {
       _x = x;
       _y = y;
   }
   private int _x, _y;
}

W czasie kompilacji dostaniemy następujący błąd: “Structs cannot contain explicit parameterless constructors”. Dlaczego więc, w przeciwieństwie do klas, nie możemy implementować bezparametrowych konstruktorów?

Główną przyczyną jest fakt, że dla każdej struktury generowany jest już domyślny konstruktor, odpowiedzialny za inicjalizowanie wartości. Warto zaznaczyć, że jest to bardzo wydajny konstruktor.  Rozważmy ponownie, inny problem z inicjalizacją:

public struct SampleStruct
{   
   public SampleStruct(int x)
   {
       _x = x;
   }
   public int _x, _y;
}

Stworzyliśmy własny konstruktor inicjalizujący tylko jedno pole. Co się stanie podczas kompilacji? Oczywiście otrzymamy błąd:  “Field ‘WindowsFormsApplication2.SampleStruct._y’ must be fully assigned before control is returned to the caller”.  Inicjalizowanie wszystkich pól jest zatem obowiązkiem konstruktorów.

Wywołanie domyślnego konstruktora, zawsze gwarantuje wyzerowanie wszystkich pól w sposób bardzo efektywny. Załóżmy, że chcemy stworzyć dużą tablicę:

SampleStruct []array=new SampleStruct[5000];

Inicjalizacja tablicy struktur jest bardzo szybka – wystarczy zaalokować odpowiednią pamięć na stosie a następnie ją wyzerować (duża przewaga nad klasami). Gdyby CLR umożliwiał wywoływanie konstruktorów domyślnych stworzonych przez użytkownika, inicjalizacja byłaby po prostu wolniejsza. Najpierw nastąpiłaby alokacja pamięci a potem w drugiej pętli każdy konstruktor musiałby zostać wywołany – a w końcu w ValueType nie o to chodzi.

Tak naprawdę CLR pozwala na definiowanie domyślnych konstruktorów. Jednak i tak nie są one wywoływane (zawsze jest wywołana inicjalizacja wygenerowana przez kompilator). Z tego względu, c# ograniczył tą możliwość aby nie wprowadzać niepotrzebnego zamieszania.

Boxing i unboxing.

W zeszłym poście pisałem o strukturach w c#. Jednym z ważnych efektów ubocznych jest boxing oraz unboxing. Jak wiemy z poprzedniego wpisu struktury przechowywane są na stosie a klasy na stercie. Boxing to niż innego jak wrapping struktury (lub jakiekolwiek innego typu wartościowego) w obiekt przechowywany na stercie. Na przykład:

int value=0;
object boxedValue=value; // boxing

Zmienna value przechowywana jest na stosie, z kolei boxedValue na stercie (pamiętajmy o nagłówku oraz GC). Należy zwrócić szczególną uwagę na boxing gdy wykorzystujemy struktury, ponieważ nieumiejętny sposób użycia może spowodować znaczny spadek wydajności zamiast wzrost. Analogicznie unboxing to proces odwrotny:

int value=0;
object boxedValue=value; // boxing
value= (int)boxedValue;//unboxing

Warto zwrócić uwagę, że boxing jest zwykle niejawny a unboxing jawny. Podczas boxingu, wymaga jest alokacja dodatkowej pamięci. Przyjrzyjmy się więc jak wygląda nasza pamięć przed i po:

image

Struktura w pamięci znacząco się zmienia i dzieję się to kosztem wydajności. Aby udowodnić, że podczas boxing’u alokowana jest nowa pamięć rozważmy:

int i = 1;
object o = i;
i = 2;  

Po skompilowaniu okaże się, że i ma wartość 2 a o 1 – to całkowicie oddzielne zmienne. Nieprawidłowy unboxing wywołuje wyjątek InvalidCastException:

int i = 1;
object o = i;
try
{
 double doubleValue = (double) o;
}
catch (System.InvalidCastException e)
{

}

Podczas unboxingu można zatem wyróżnić dwie czynności:

  1. Sprawdzenie czy owinięty typ jest taki sam jak ten na który rzutujemy. Jeśli typy nie zgadzają się, wyrzucany jest wyjątek InvalidCastException.
  2. Alokacja nowej pamięci na stosie oraz z kopiowanie wartości.

Z przedstawionych powyżej powodów, używanie kolekcji ArrayList jest bardzo złym podejściem. Lepszym rozwiązaniem jest wprowadzanie typów generycznych. Stosownie do MSDN, boxing jest 20 razy wolniejszy a unboxing 4 razy wolniejszy niż tradycyjne rozwiązania niewymagające tych mechanizmów.

Klasy i struktury w C#

Znajomość różnić między klasą a strukturą w c# jest bardzo ważna ze względu na kwestie wydajnościowe. Sposób wykorzystania może czasami wydawać się bardzo podobny, jednak to co dzieje się w tle jest kompletnie różne. W przypadku C++, struktury i klasy były bardzo podobne i reprezentowane były w pamięci w taki sam sposób. Różnica polegała na tym, że pola klas domyślnie były prywatne, a struktur publiczne.

Przede wszystkim struktura jest wartością (value type) a klasa typem referencyjnym. Value Type jest alokowany na stosie, z kolei typ referencyjny na stercie (heap). Każda klasa dziedziczy po  System.Object . Struktura dziedziczy po System.ValueType, który z kolei również pochodzi od System.Object.

Należy pamiętać, że parametry które są Valuetype podczas przekazywania do metod są w całości kopiowane. W przypadku klas, kopiowany jest wyłącznie adres komórki w którym wartości danej klasy znajdują się. Podobnie jest z wszelkimi operacjami przypisania:

int a,b;
a=5;
b=a; // a i b to dwie różne komórki w pamięci 
       //(bez optymalizacji kompilatora)

SampleClass a = new SampleClass();
SampleClass b;
b=a; // b wskazuje na a i obie 
     // referencje wskazują na ten sam fragment pamięci

Szczególnie niebezpiecznie jest połączenie właściwości (getter) z strukturami – za każdym razem tworzona będzie nowa struktura.

Każdy obiekt klasy zajmuje więcej pamięci ponieważ alokowany jest na stercie i oprócz właściwych danych, miejsce przeznaczane jest również na nagłówek. Zatem jeśli typ składa się z dwóch Int32 dla struktury będzie zajmować 64 bity a dla klasy 64 + header.

Oprócz względów pamięciowych, głównym problemem klas jest zwalnianie pamięci. Garbage Collector nie jest najszybszym rozwiązaniem zatem  faza “marking” dla GC zajmuje trochę czasu, zwłaszcza dla dużych obiektów, z wysoką liczbą zagnieżdżonych referencji.

Podsumowujmy więc zasadnicze różńcie:

1. Z jakiego typu dziedziczy?

Klasa: System.Object

Struktura: System.ValueType, który z kolei pochodzi od System.Object.

2. Jak są wykonywane przypisania? 

Klasa jest typem referencyjnym (tylko adres jest kopiowany). W przypadku struktury, wszystkie pola są kopiowane.

3. Czy wspiera typy zagnieżdżone?

Zarówno klasy jak i struktury wspierają typy zagnieżdżone:

struct SampleStructA
{
   public int a, b, c;
   public SampleStructB structB;

   public struct SampleStructB
   {
       public int a, b, c;
   }
}

4.  Czy dozwolone są pola oraz ich modyfikatory

Jak widać na powyższym przykładzie struktury również mogą zawierać pola. Ze względu na sposób inicjalizacji struktur (o tym w następnym poście), nie można definiować wartości domyślnych:

struct SampleStructA
{
    public int a = 9; // ŹLE
}

5. Czy dozwolone są stałe?

Zwykłe pola nie mogą zawierać wartości domyślnych, jednak stałe są dozwolone:

struct SampleStructA
{
   public const int a = 9;
}

W przypadku klas, pola jak i stałe mogą być deklarowane bez żadnych ograniczeń.

6. Czy dozwolone są właściwości, zdarzenia, indexer’y oraz metody?

Tak – deklaracja niczym się nie różni się od klas. Jednak należy być świadomym kilku pułapek. Struktury są zawsze kopiowane, zatem poniższa konstrukcja jest niebezpieczna:

internal struct SampleStructB
{
   private SampleStructA _structA;

   public SampleStructA SampleStructA
   {
       get { return _structA; }
   }
}

    

Za każdym razem gdy chcemy uzyskać dostęp przez SampleStructA, _structA jest kopiowany. Ze zdarzeniami również należy być ostrożnym. W praktyce jednak nie znam przypadku w którym musiałem użyć zdarzeń w strukturze.

7. Czy dozwolone są pola statyczne?

Tak

8. Wspiera dziedziczenie?

Wszystkie struktury  są sealed(nie można dziedziczyć). Można jednak implementować interfejs.

9. Wsparcie dla typów generycznych, przeładowania operatorów oraz słowa kluczowego partial 

Zarówno klasy jak i struktury wspierają powyższą funkcjonalność.

11. Czy można definiować konstruktor?

Konstruktory są dozwolone dla struktur, jednak nie ma możliwości stworzenia domyślnego  konstruktora(bez parametrów).

12. Czy można przypisać NULL?

NULL to wskaźnik na pusty adres. Struktury są wartościami, więc nie ma takiej możliwości. Można jednak skorzystać z Nullable i cieszyć się podobnym zachowaniem:

SampleStructB? sampleStruct=null;
// LUB
Nullable<SampleStructB> sampleStruct=null;
            

10. Co z operatorem new?

W przypadku klas sprawa jest jasna – należy używać new aby zainicjalizować obiekt. Operator new jest jednak również dozwolony dla struktur:

SampleStructB sampleStruct=new SampleStructB();
sampleStruct.a = 4;
MessageBox.Show(sampleStruct.a.ToString());
MessageBox.Show(sampleStruct.b.ToString());

Różnicą jednak jest fakt, że new nie jest jedynym operatorem inicjalizacji. Można również obyć się bez niego:

SampleStructB sampleStruct;
sampleStruct.a = 4;
MessageBox.Show(sampleStruct.a.ToString());

Nie spowoduje to wyjątku NullReference. Należy jednak pamiętać, aby ręcznie zainicjalizować WSZYSTKIE publiczne pola, z których chcemy skorzystać. Poniższy kod nie skompiluje się ponieważ pole b nie zostało ręcznie zainicjalizowane:

SampleStructB sampleStruct;
sampleStruct.a = 4;
MessageBox.Show(sampleStruct.a.ToString());
MessageBox.Show(sampleStruct.b.ToString());

Pozostało jeszcze kilka kwestii do omówienia: boxing oraz wyjaśnienie dlaczego nie można definiować domyślnych konstruktorów – o tym w następnych postach.

Ze względów wydajnościowych warto zatem rozważyć zapomniane  struktury, szczególnie dla małych, niezmiennych fragmentów danych (Point, Vector itp.).

MEF: import, część III

W dzisiejszym poście kilka słów i imporcie implementacji. W poprzednich postach, pokazałem jak dokonywać prostych importów. Dzisiaj trochę więcej szczegółów. Dla przypomnienia, aby zaimportować (wstrzyknąć) implementację, w najprostszym przypadku używamy po prostu atrybutu Import:

public class SampleViewModel
{
    //...
    [Import]
    public IMessageBoxService MessageBoxService{get;set;}
}

Powyższy przykład reprezentuje wstrzyknięcie właściwości. W podobny sposób można wstrzykiwać parametry dla konstruktora:

public class SampleViewModel
{
    [ImportingConstructor]
    public SampleViewModel(IMessageBoxService service)
    {
        MessageBoxService = service;
    }
    public IMessageBoxService MessageBoxService{get;private set;}
}

Podczas importu SampleViewModel, automatycznie zostanie uruchomiony konstruktor importujący (IMessageBoxService również zostanie wstrzyknięty). Parametry wejściowe można oznaczyć atrybutem Import jeśli korzystamy z niestandardowego kontraktu.

Wcześniej nie zostało to pokazane, ale również zwykłe pola mogą zostać wstrzykiwane:

public class SampleViewModel
{
    //...
    [Import]
    private IMessageBoxService _messageBoxService;
}

Domyślnie, jeśli w kontenerze nie ma implementacji, import zakończy się wyjątkiem. Jeśli nasza architektura przewiduje, że kontener może być pusty wtedy możemy skorzystać z właściwości AllowDefault:

public class SampleViewModel
{
    //...
    [Import(AllowDefault=true)]
    public IMessageBoxService MessageBoxService{get;set;}
}

Jeśli  w kontenerze istnieje kilka implementacji, wtedy standardowy import zostanie również zakończony wyjątkiem – nie wiadomo której implementacji użyć. Możemy jednak zażądać wszystkich implementacji za pomocą atrybutu ImportMany:

public class SampleViewModel
{
    //...
    [ImportMany]
    public IMessageBoxService[] AllServices{get;set;}
}

Na zakończenie, warto wspomnieć o interfejsie  IPartImportsSatisfiedNotification, który posiada jedną metodę  OnImportsSatisfied, uruchamianą po imporcie wszystkich elementów danej klasy:

 public class SampleViewModel : IPartImportsSatisfiedNotification
 {
    // lista importowanych części

    public void OnImportsSatisfied() 
    {
      // wszytkie elementy zostały zaimportowane.
    } 
 }

MEF: eksport, część II

W ostatnim poście przedstawiłem MEF wraz z prostym przykładem. Dzisiaj przyjrzymy się dokładniej sposobom eksportu. Znamy już podstawowy sposób eksportu klasy:

[Export(typeof(IMessageBoxService))]
public class WpfMessageBoxService:IMessageBoxService
{
}

Oprócz przekazania typu, można także użyć zwykłego string’a. Ze względu jednak na łatwość popełnienia literówki lepszym sposobem jest użycie typeof. Oprócz klas można również eksportować właściwości np.:

public class Order
{
   [Export("OrderValue")]
   public int Value { get { return 3; } }
}
public class OrderInvoker
{
   [Import("OrderValue")]
   public int Value { get; set; }
}

Po imporcie, OrderInvoker.Value będzie miał wartość 3.Łatwo to przetestować używając poniższego kodu:

AssemblyCatalog catalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());
CompositionContainer compositionContainer = new CompositionContainer(catalog);

OrderInvoker orderInvoker=new OrderInvoker();

// wyświetli 0
MessageBox.Show(orderInvoker.Value.ToString());
compositionContainer.ComposeParts(orderInvoker);            
// wyświetli 3
MessageBox.Show(orderInvoker.Value.ToString())

Również metody można eksportować:

public class Order
{
   [Export("Submit")]
   public void Submit()
   {
       MessageBox.Show("Submit");
   }
}
public class OrderInvoker
{
   [Import("Submit")]
   public Action SubmitInvoker { get; set; }

   public void Submit()
   {
       SubmitInvoker();
   }
}

Test:


AssemblyCatalog catalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());
CompositionContainer compositionContainer = new CompositionContainer(catalog);

OrderInvoker orderInvoker=new OrderInvoker();            
compositionContainer.ComposeParts(orderInvoker);            

// wyswietli msg
orderInvoker.Submit();

Ostatnią kwestią jest atrybut InheritedExport. Dodany do klasy bazowej, powoduje, że wszelkie klasy dziedziczące zostaną automatycznie wyeksportowane. Wystarczy, że klasa bazowa jest oznaczona InheritedExport:

[InheritedExport]
interface IAnyInterface
{
}
// Nie musimy już używać atrybutu Export, 
// ponieważ bazowy interfejs został oznaczony
// InheritedExport
public class AnyClass:IAnyInterface
{
}

Managed Extensibility Framework część I

MEF jest kolejnym frameworkiem umożliwiającym tworzenie rozszerzalnych aplikacji. Pomijając marketing serwowany na różnych stronach, jest to po prostu biblioteka umożliwiająca dependency injection. W wersji .NET 4.0 została zintegrowana w .NET Framework i nie musi być instalowana osobno. Na oficjalnej stronie można sporo poczytać o budowie MEF’a jednak w poście chciałbym ograniczyć to do minimum ponieważ dla mnie osobiście, zbyt dokładne intro do tematu po prostu zniechęca mnie do dalszego czytania.

Zacznijmy więc od przykładu! Chcemy napisać aplikację, która zawiera usługę umożliwiającą wyświetlanie wiadomości. Ponadto chcemy to zrobić w sposób maksymalnie elastyczny tak aby implementacja mogła być wstrzykiwana dynamicznie – umożliwi nam to późniejsze testy jednostkowe.  Najpierw dołączamy referencje do MEF:

image

 

Następnie definiujemy interfejs (kontrakt):

public interface IMessageService
{
    void Show(string message,string caption);
}

Oraz dwie implementacje (prawdziwa dla wpf oraz stub dla unit test):

public class WpfMessageBoxService:IMessageBoxService
{
    public void Show(string message,string caption)
    {
        MessageBox.show(caption,message);
    }
}
public class StubMessageBoxService:IMessageBoxService
{
    public void Show(string message,string caption)
    {
        // do nothing
    }
}

Następnie gdzieś w kodzie, np. w ViewModel mamy właściwość reprezentującą usługę:

public class SampleViewModel
{
    //...
    public void Show()
    {
        // jakas logika
        MessageBoxService.Show("...","...");
    
    }
    
    public IMessageBoxService MessageBoxService{get;set;}
}

Przedstawiona architektura umożliwia wstrzyknięcie dowolnej implementacji do  MessageBoxService . W klasycznych IoC dostęp do konkretnej implementacji uzyskujemy np. poprzez:

this.MessageBoxService = container.Resolve<IMessageBoxService>();

Za pomocą MEF jest to jak najbardziej możliwe. Jednak przeważnie wykorzystuje się atrybuty: Export aby wyeksportować konkretną implementację do wspólnego kontenera, oraz Import aby ją wstrzyknąć. Zatem implementacje powinny wyglądać następująco:

[Export(typeof(IMessageBoxService))]
public class WpfMessageBoxService:IMessageBoxService
{
    public void Show(string message,string caption)
    {
        MessageBox.show(caption,message);
    }
}
[Export(typeof(IMessageBoxService))]
public class StubMessageBoxService:IMessageBoxService
{
    public void Show(string message,string caption)
    {
        // do nothing
    }
}

Z kolei polecenie wstrzyknięcia dokonujemy za pomocą Import:

public class SampleViewModel
{
    //...
    public void Show()
    {
        // jakas logika
        MessageBoxService.Show("...","...");
    
    }
    [Import]
    public IMessageBoxService MessageBoxService{get;set;}
}

Ostatnia zagadka, jak import jest powiązany eksportem? Możemy w końcu atrybutem export oznaczyć wiele różnych implementacji. Skąd MEF ma wiedzieć czy WpfMessageBoxService powinien zostać użyty czy StubMessageBoxService?

Do tego służą katalogi oraz metoda ComposeParts. Najpierw definiujemy katalog zawierający wszelkie klasy opatrzone atrybutem Export, a potem wywołujemy ComposeParts na klasie, która zawiera atrybuty Import. Jeśli istnieje kilka implementacji w katalogu, wtedy Import wyrzuci wyjątek (ponieważ Import w przeciwieństwie do ImportMany stanowi relację jeden do jednego). MEF wspiera kilka typów katalogów:

  1. AssemblyCatalog – ładuje bibliotekę i w katalogu pluginów umieszczane są wszystkie klasy opatrzone Export.
  2. DirectoryCatalog – ładuję wszystkie biblioteki zawarte w danym katalogu Windows i szuka w nich klas opatrzonych Export.
  3. Type Catalog – jawne wczytanie danej klasy do kontenera.
  4. Aggregate Catalog – kolekcja zawierająca dowolną ilość katalogów (np. TypeCatalog oraz AssemblyCatalog).

Ostatnim więc krokiem jest stworzenie katalogu i skonstruowanie SampleViewModel (wstrzyknięcie implementacji):

TypeCatalog typeCatalog=new TypeCatalog(typeof(WpfMessageBoxService));
var container = new CompositionContainer(typeCatalog);
container.ComposeParts(sampleViewModel);

Po wywołaniu ComposeParts, MessageBoxService będzie zawierał WpfMessageBoxService. Oczywiście dużo ciekawszym jest załadowanie pluginów  z bibliotek wewnętrznych np.:

DirectoryCatalog directoryCatalog=new DirectoryCatalog("bin\\plugins");
AssemblyCatalog assemblyCatalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());

AggregateCatalog aggregateCatalog=new AggregateCatalog();
aggregateCatalog.Catalogs.Add(directoryCatalog);
aggregateCatalog.Catalogs.Add(assemblyCatalog);

var container = new CompositionContainer(aggregateCatalog);
container.ComposeParts(sampleViewModel);

W kolejnych postach przedstawię bardziej szczegółowo zagadnienia związane z eksportem i importem typów.

NULL Object Pattern (Special Case)

Wartość NULL w programowaniu stanowi specjalny przypadek. Jakiekolwiek operację na null spowodują wyrzucenie wyjątku. W pewnych przypadkach uzasadnione jest użycie obiektu zamiast zamiast klasycznych null.  Stanowi to kolejny krok w stronę programowania obiektowego – tym razem nawet NULL jest obiektem.

Załóżmy, że na stronie internetowej możemy oceniać różne produkty i nasz dotychczasowy kod wygląda następująco:

Product product = repository.GetProductById(4);
if( product != null)
    product.RateIt(userRating);

Zawsze musimy sprawdzać czy GetProductById nie zwrócił null. W przypadku NULL object pattern, tworzymy nowy obiekt dla wartości NULL:

abstract class ProductBase
{
    public abstract void RateIt(int value);
}
class Product:ProductBase
{
    public override RateIt(int value)
    {
        // aktualziacja oceny
    }
}
class NullProduct:ProductBase
{
    public override RateIt(int value)
    {
        //do nothing
    }
}

Teraz nie musimy już martwić się o NULL:

ProductBase product = repository.GetProductById(4);
product.RateIt(userRating);

Oczywiście należy odpowiednio zaktualizować GetProductById aby zwracać NullProduct zamiast klasycznego null.  Koniecznie w NullObject należy przeładować wszelkie metody i właściwości tak aby obiekt odróżniał się od prawdziwych obiektów.

Warto jednak zastanowić się czy NULL object pattern jest dobry dla danego scenariusza. Zauważyłem, że w niepoprawnym kodzie, często wyłącznie przykrywa problem. Jeśli w danym scenariuszu nie rozważamy NULL lepiej aby aplikacja wyrzuciła wyjątek niż działała dalej, ukrywając tym samym jakiś błąd. Dobrym zwyczajem jest również zaimplementowanie wzorca jako Singleton.