Pointer czyli trzeci bliźnaik

Chyba każdy programista piszący w C# usłyszał kiedyś (najczęściej na rozmowie kwalifikacyjnej) pytanie o typy danych występujących w tym języku. Naturalną odpowiedzią są typy wartościowe i typy referencyjne (taka nierozłączna para bliźniąt), mówimy jakie są różnice pomiędzy nimi, mówimy mądre zdania o stosie i o stercie. Czasem dostajemy “podchwytliwe” pytanie w stylu:

String to typ referencyjny czy wartościowy?
String to jest przechowywany na stosie czy na stercie?
Tablica intów to typ referencyjny czy wartościowy?
Enum to ty wartościowy czy referencyjny?
Jak działa Garbage Collector?

Odpowiadamy na te pytania (i następne, następne … ) po czym dostajemy wymarzoną pracę i…(stop! to nie jest post o tym, może innym razem).
Jeśli ktoś w tym momencie myśli, że wystarczy wykuć pewne regułki zgodnie z obowiązującą na studiach zasadą ZZZ (albo ZZZZ w zależności od uczelni) to niech lepiej szybko zmieni swoje zdanie. Bo ważniejszym jest zrozumieć inaczej będziemy pisać kod, który nie będzie działał zgodnie z naszymi planami np:

DateTime bastilleDay = new DateTime(1789, 7, 14);
bastilleDay.AddYears(250);
Console.WriteLine(bastilleDay.ToShortDateString());

Kiedy będziemy obchodzić 250 rocznice zdobyci Bastylii? Jeśli ktoś myśli, że ten kod mu to powie to, się grubo myli(jeszcze gorzej jeśli nie wie dlaczego).
Wracając do meritum. Gdzie więc zlokalizować z pointera czyli tytułowego trzeciego bliźniaka i co to takiego w ogóle jest?
W dużym uproszczeniu pointer to zmienna, która trzyma adres do innej zmiennej i radzę zrozumieć, że jest to coś zupełnie innego niż referencja do innej zmiennej (vide typ referencyjny). Pointer jest uznawany za niebezpieczny typ więc aby go użyć należy przebić się przez podwójną gardę:

  1. Należy włączyć obsługę kodu niebezpiecznego (“allow unsafe code” we właściwościach projektu)
  2. Należy określić blok kodu jako unsafe lub określić całą metodę jako unsafe

Czas może na jakiś przykład.

int x = 100;
int y = x;
int* z = &x;

najpierw prosta sprawa mamy zmienną x do której kopiujemy(przypisujemy) wartość 100, do zmiennej y kopiujemy zawartość zmiennej x, a do zmiennej z adres zmiennej x. Nieznającym składni (o ile tacy są) radzę zwrócić uwagę na nią uwagę:

  • int* czyli wskaźnik na inta
  • &x adres zmiennej x

no to teraz się zabawmy

x++;

jaka będzie wartość poszczególnych zmiennych? Komuś może się to wydać trywialne(ulubione słówko pewnego mojego wykładowcy) ale spróbujmy:

Console.WriteLine(x);  //101
Console.WriteLine(y);  //100 
Console.WriteLine(*z); //101 

Zaskoczeni (mam nadzieje że nie)? To teraz w drugą stronę.

*z+=1;
Console.WriteLine(x);  //102
Console.WriteLine(y);  //100 
Console.WriteLine(*z); //102

To było chyba oczywiste. To może taka zagadka co wypiszę ta komenda:

Console.WriteLine(sizeof(int*));

Ciekawe czy ktoś zna już odpowiedź (niestety nie przewiduje żadnych nagród).

No dobra, ale to był pointer użyty do prostego typu wartościowego. Spróbujmy zrobić pointera do czegoś bardziej zaawansowanego np. takiej oto struktury:

struct Point
{
  public int X;
  public int Y;
}

To jak teraz wykorzystać pointera

Point p;
p.X = 0;
p.Y = 0;
Console.WriteLine("X={0}; Y={0}", p.X, p.Y); //X=0; Y=0;
Point* pointAdress;
pointAdress = &p;
pointAdress->X = 100;
pointAdress->Y = 150;
Console.WriteLine("X={0}; Y={0};",p.X,p.Y); //X=100; Y=150;

pointAdress->X informuje nas o tym, że chcemy uzyskać dostęp do pola X struktury, której adres wskazuje pointAdress. Innym sposobem zapisu tego samego byłoby (*pointAdress).X – to już kwestia indywidualna. Klasy obsługuję się w “podobny” sposób. Dlaczego podobny a nie taki sam? Otóż na początku wspominałem o coś o stosie i stercie oraz o Garbage Collectorze. I właśnie tutaj przydaje się nam zrozumienie podstawowych mechanizmów C# (i ogólnie .Netu). Gdybyśmy użyli pointera do wskazania na stertę (bez użycia wspomagania) to jaką pewność będziemy mieć, że po pewnym czasie w danym miejscu sterty dalej znajduje się nasza klasa? Żadnej ponieważ stertą zarządza Garbage Collector (polecam cykl Piotra Zielińskiego na ten temat). Tak napisany kod się nie nawet skompiluje. W tym celu używamy słowa kluczowego fixed które informuje GC, aby pozostawiło niezmieniony adres np:

class Person
{
  public int Age;
}
//gdzieś dalej w kodzie

Person person=new Person();
fixed(int* ageAddress= &person.Age)
{

}

Istniej również słowo kluczowe stackalloc, które może w pewnych przypadkach zastąpić fixed (ale tutaj też odsyłam do Piotra Zielińskiego).

Zastosowanie

Wszystko fajnie tylko, kiedy tego używać i czy w ogóle jest sens. To zależy. Nie zawsze jest czas, nie zawsze jest potrzeba albo możliwości. Spróbuje teraz pokazać przykładowe zastosowanie pointera. Nie będę silił się na oryginalność i pokaże zmieniony (uproszczony) projekt, który kiedyś dostarczyłem na zajęcia (kod dostępny na githubie). Zadaniem programu była manipulacja obrazem. Tutaj ograniczyłem się do zmiany obrazu poprzez z wykorzystaniem skali szarości. Użyłem najprostszego algorytmu (suma składowych kolorów dzielona na trzy). Dla małych obrazów różnica w czasie ze względu na zastosowaną metodę jest nieistotna. Jednak dla większych zdjęć różnica może być kolosalna.
ConvertToGreyScale
16 sekund dla 15Mpix zdjęcia to trochę jest ale gdy zastosowałem pointera konwersja zajęła niecałą sekundę. Jest nad czym myśleć(oczywiście kod, który przedstawiłem nie jest do końca zoptymalizowany i odporny na błędy, ale chodziło mi o przedstawienie ogólnej koncepcji użycia wskaźników w C#).

* Prawidłowa odpowiedź brzmi to zależy. Jeśli ktoś odpowiedział osiem bajtów bo samemu odpalił kod to ma rację, ale rację może też mieć ten kto odpowie cztery bajty. Wszystko zależy od tego na co ustawimy target naszego projektu. Komenda sizeof() inaczej zachowa się przy projekcie ustawionym na x64 a inaczej na x86 (to chyba całkiem logiczne).

4 thoughts on “Pointer czyli trzeci bliźnaik

  1. Pingback: dotnetomaniak.pl
  2. Wszystko pięknie i fajnie, tylko nie za bardzo zrozumiałem jak ten przykład z DateTime ma się do tematu. AddYears zwraca nową instancję struktury DateTime i tutaj tkwi pomyłka – piszący kod nie doczytał dokumentacji (bądź nie spojrzał w podpowiedź Intellisense). Jestem przekonany, że przykład jakoś się wpisuje w treść artykułu, ale nie jestem pewien jak – czy mógłbym prosić o dopowiedzenie?

    1. Faktycznie przekombinowalem z tym przykładem. Chodziło mi o to, że konieczne jest zrozumienie pewnych podstaw (mimo, że Intellisense podpowiada).

  3. Co do przykładu z DateTime to jako ciekawostkę dodam że w javie by zadziałało i to sprawiło że chłopaki w Sun, a potem w Oracle cierpieli na ciężkie przypadki migreny związanej z wątkami. Dopiero w Javie 8 z pudełka pojawił się typ dla Dat korzystający z wzorca ValueObject.

Comments are closed.