Posts tagged ‘C#’

Of Languages and Keywords

2012-05-28 18:40

I recently had a discussion with a co-worker about feasibility of using anonymous functions in Python. We happen to overuse them quite a bit and this is not something I’m particularly fond of. For me lambdas in Python are looking pretty weird and thus I prefer to use them sparingly. I wasn’t entirely sure why is it so – given that I’m quite a fan of functional programming paradigm – until I noticed a seemingly insignificant fact.

Namely: the lambda keyword is long. With six letters, it is among the longer keywords in Python 2.x, tied with return, import and global, and beaten only by continue and finally. Quite likely, this is what causes lambdas in Python to stand out and require additional mental resources to process (assuming we’re comfortable enough with the very idea of anonymous functions). The long lambda keyword seems slightly out of place because, in general, Python keywords are short.

Or are they?… Thinking about this, I’ve got an idea of comparing the average length of keywords from different programming languages. I didn’t really anticipate what kind of information would be exposed by applying such a metric but it seemed like a fun exercise. And it surely was; also, the results might provoke a thought or two.

Here they are:

Language Keyword Total chars Chars / keyword
Python 2.7 31 133 4.29
C++03 74 426 5.76
C++11 84 516 6.14
Java 1.7 50 289 5.78
C 32 166 5.19
C# 4.0 77 423 5.49
Go 25 129 5.16

The newest incarnation of C++ seems to be losing badly in this competition, followed by C#. On the other side of the spectrum, Go and Python seem to be deliberately designed to avoid keyword bloat as much as possible. Java is somewhere in between when it comes to sheer numbers of keywords but their average length is definitely on the long side. This could very well be one of the reasons for the perceived verbosity of the language.

For those interested, the exact data and code I used to obtain these statistics are in this gist.

Tags: , , , , ,
Author: Xion, posted under Computer Science & IT » 8 comments

Klasy bardziej meta

2011-11-08 21:57

W językach ze skrzywieniem obiektowym klas używa się często i gęsto. Nieco inaczej jest z innym, o wiele mniej znanym pojęciem: metaklasami. Przedrostek meta- sugeruje tu od razu nieco wyższy poziom abstrakcji, co jest generalnie słusznym podejrzeniem. Dokładne znaczenie kryjące się za tym terminem w pewnym stopniu zależy od języka programowania, lecz w każdym przypadku chodzi o narzędzie związane z manipulowaniem samymi klasami. Zupełnie intuicyjnie jest to więc “jeden poziom meta więcej” :)
Jako nieco bardziej zaawansowana funkcjonalność, metaklas nie wykorzystuje się codziennie. Tym bardziej jednak warto wiedzieć przynajmniej z grubsza, jak działają, aby poprawnie rozpoznawać sytuacje, w których są one użyteczne. I dlatego właśnie dzisiaj przyjrzymy się metaklasom w trzech wariantach, specyficznych dla popularnych języków programowania.

Pierwszy z nich dotyczy systemów refleksji, czyli przydatnego mechanizmu językowego, pozwalającego programowi na wgląd w swoją wewnętrzną strukturę. W językach obiektowych oznacza to przede wszystkim możliwość posługiwania się klasami na bardziej dynamicznym poziomie. Są one wtedy reprezentowane przez obiekty, i właśnie te reprezentacje – używane poprzez moduły refleksji – nazywa się czasem metaklasami.
W wielu językach wyglądają one podobnie. Java ma na przykład klasę java.lang.Class, zaś C# – System.Type. Zyskując dostęp do instancji tychże klas (np. poprzez Class.forName albo Type.GetType) otrzymujemy możliwość wykonywania operacji na klasach, które one reprezentują. I tak możliwe jest chociażby tworzenie ich obiektów, wywoływanie metod, dostęp do pól. Dzięki temu możemy na przykład w (miarę) łatwy sposób zaimplementować proste rozwiązania wspierające pluginy, tj. dynamicznie ładowane wtyczki do naszych aplikacji.

Drugie znaczenie pojęcia metaklasy opisuje referencje do klas. Ideą jest tu traktowanie klas jako wartości pierwszego rodzaju (first-class value), które można przypisywać do zmiennych i w uogólniony sposób przekazywać między różnymi miejscami w kodzie. Specjalne zmienne, do których możemy “przypisywać” klasy są w co najmniej jednym języku nazywane właśnie metaklasami.
Przydatność tego typu rozwiązania zależy głównie od tego, jak wielkimi jesteśmy fanami wzorców typu fabryka abstrakcyjna :) Być może podstawową funkcjonalnością takich metaklas jest bowiem abstrakcja procesu tworzenia (lub ogólniej: pozyskiwania z zewnątrz) nowych obiektów. Pozostałe przypadki użycia metaklas są pokrywane zwykle przez szablony (C++), typy generyczne (C#, Java, itp.) lub podobne mechanizmy tworzenia kodu, który w pewien określony sposób jest niezależny od dokładnego typu obiektów, którymi operuje.

Czas wreszcie na trzeci wariant metaklas. Jest on zdecydowanie najciekawszy, ale też najbardziej skomplikowany i w pewien sposób wysublimowany – mimo swojej niewątpliwej użyteczności. Mam tutaj na myśli metaklasy w Pythonie.

Tags: , , , , ,
Author: Xion, posted under Programming » 1 comment

Proste haszowanie obiektów

2011-10-15 19:37

W językach wspierających obiektowość zwykle mamy do czynienia z uniwersalną klasą bazową (Object), od której wszystkie inne muszą dziedziczyć. Składniki tej klasy są więc wspólne dla wszystkich obiektów. Czasami ma to negatywne skutki (vide wait i notify w Javie), ale zazwyczaj jest przydatne, bo wspólne składniki – takie jak toString – są często niezwykle użyteczne.
Istnieje jednak pewna metoda bazowa obiektów, której przeznaczenie nie musi być od razu oczywiste. Nazywa się ona dość podobnie w różnych językach: GetHashCode w C#, hashCode w Javie, zaś w Pythonie jest to po prostu __hash__. Nietrudno się więc domyślić, że ma ona coś wspólnego z hashem obiektu :) O co jednak dokładnie chodzi?

Hash to kompaktowa reprezentacja pewnej porcji danych, uzyskana za pomocą określonego algorytmu. Takim algorytmem może być na przykład MD5, SHA1, SHA256 i inne. Podstawowym wymaganiem, jakie stawia się takim funkcjom, jest determinizm: dla identycznych danych powinny one zwracać dokładnie te same wyniki.
Do czego jednak przydaje się możliwość haszowania< dowolnych obiektów? Otóż pozwala to na tworzenie kontenerów haszujących, takich jak zbiory i mapy. W Javie na przykład chodzi tu o pojemniki HashSet i HashMap, będące jednymi z możliwych implementacji ogólnych interfejsów Set i Map. Takie kontenery używają hashy jako podstawy swojego działania, zakładając, że ich porównywanie jest szybsze niż branie pod uwagę całych obiektów. Dopiero równość ich hashy pociąga za sobą konieczność porównania samych obiektów.

Być może nasuwa się tu od razu słuszny wniosek, że hashe dla obiektów potrzebne są wobec tego jedynie wtedy, gdy implementujemy własny sposób ich porównywania. (Opisywałem kiedyś, jak to się robi w języku C#). Często powinniśmy wtedy zapewnić taką implementację ekwiwalentu metody hashCode, która będzie spójna ze sposobem porównywania. Z grubsza chodzi o to, aby na hash wpływały pola, które bierzemy pod uwagę w metodzie equals/Equals/__eq__ – i tylko one.

W jaki sposób miałyby jednak to robić? No cóż, w teorii możemy zaprząc do pracy wymienione wyżej algorytmy i potraktować nimi połączone wartości pól obiektu (albo po prostu kawałek pamięci, w którym on rezyduje). W praktyce to bardzo kiepskie rozwiązanie (zwłaszcza wydajnościowo), bowiem wspomniane funkcje haszujące niezupełnie do tego służą. Istnieje bowiem różnica między kryptograficzną funkcją haszującą a zwykłą: ta pierwsza ma na celu przede wszystkim uniemożliwienie odtworzenia oryginalnych danych, jeśli znany jest tylko ich hash. W przypadku funkcji używanych wraz z pojemnikami haszującymi bardziej interesuje nas jednorodność, co (w uproszczeniu) oznacza, że hash obiektu powinien być wrażliwy na wartości poszczególnych pól tego obiektu. Z tego też powodu poniższe rozwiązanie:

  1. @Override
  2. public int hashCode() {
  3.     return 42;
  4. }

jest do niczego, mimo że świetnie spełnia teoretyczne wymaganie, aby dwa równe obiekty miały równe hashe.

Dostępna jest oczywiście wyrafinowana wiedza na temat konstruowania dobrych (a nawet doskonałych) funkcji haszujących, ale dokładnie rachunki prawdopodobieństwa kolizji nieczęsto nas interesują – zwłaszcza, jeśli właściwie nie wiemy, w jakich pojemnikach i wśród jakich innych obiektów skończą te nasze. Na szczęście mamy też prostsze warianty. Wśród nich interesująco wygląda na przykład sposób pokazany w znanej książce Effective Java, który wygląda mniej więcej w ten sposób:

  1. Zaczynamy od dowolnej liczby dodatniej.
  2. Dla każdego znaczącego pola w obiekcie:
    1. bierzemy jego 32-bitową reprezentację (w razie potrzeby używając operacji bitowej xor dla większych pól)
    2. dodajemy ją do wyniku, uprzednio pomnożonego przez małą liczbę pierwszą

Zastosowanie go np. do prostej klasy punktu 3D wyglądałoby na przykład tak:

  1. public class Point3D {
  2.     private float x, y, z;
  3.     // ...
  4.     @Override public int hashCode() {
  5.         int res = 23;
  6.         res = 31 * res + Float.floatToIntBits(x);
  7.         res = 31 * res + Float.floatToIntBits(y);
  8.         res = 31 * res + Float.floatToIntBits(z);
  9.         return res;
  10.     }
  11. }

Wybór 23 jest raczej arbitralny, natomiast 31 ma tę zaletę, że mnożenie przez nią liczby x jest równoważne przesunięciu bitowemu i odejmowaniu, tj. (x << 5) - x. Analogicznie jest zresztą dla innych liczb o jeden mniejszych od potęg dwójki.

Tags: , , ,
Author: Xion, posted under Programming » Comments Off on Proste haszowanie obiektów

Drugi problem, czyli API do wyrażeń regularnych

2011-05-19 22:54

Napotykając problem, niektórzy ludzie myślą: “Użyję wyrażeń regularnych!”
W rezultacie mają dwa problemy.

Jamie Zawinski @ alt.religion.emacs

Ten słynny cytat jest, według mnie, lekkim niedoszacowaniem. Decydując się na skorzystanie z wyrażeń regularnych, z miejsca dostajemy bowiem dwa problemy z nimi samymi :) Pierwszych z nich jest sama składnia, która dla nieprzyzwyczajonego oka jest cokolwiek nietrywialna. To głównie ona jest wskazywana jako główna trudność w sprawnym i efektywnym używaniu regexów.
Dzisiaj jednak chciałem zwrócić uwagę na ten drugi, rzadziej zauważany problem. Otóż samo wyrażenie to nie wszystko, trzeba je jeszcze odpowiednio użyć w naszym kodzie. I tutaj mogą zacząć się schody, bo w różnych językach programowania sprawa ta wygląda często odmiennie. Na szczęście jest da się tu też wskazać podobieństwa i spróbować dokonać uogólnienia.

Podstawowym elementem interfejsu programistycznego do wyrażeń regularnych jest zwykle obiekt wzorca (pattern), czyli samego wyrażenia. Zawiera on jego postać skompilowaną, którym jest mniej lub bardziej skomplikowana (w zależności od składni) konstrukcja przypominająca automat stanów. Zbudowanie tej wewnętrznej reprezentacji jest konieczne, aby przeprowadzić jakąkolwiek operację (np. wyszukiwania czy dopasowania). Jeśli więc planujemy skorzystać z jednego wyrażenia w celu przetworzenia większej liczby tekstów, dobrze jest posługiwać się gotowym, skompilowanym obiektem.
Ten ogólny opis dobrze przenosi się na rzeczywiste języki programowania, w których możemy znaleźć takie klasy jak:

Tekstową postać wyrażeń regularnych podajemy zwykle do konstruktorów wyżej wymienionych klas, względnie używamy jakichś statycznych lub globalnych funkcji z odpowiednich pakietów. Przy okazji warto też wspomnieć o problemie escape‘owania znaków specjalnych w wyrażeniach, który w mocno niepożądany sposób interferuje z analogicznym mechanizmem w samych językach programowania. Ponieważ w obu przypadkach używa się do tego znaku backslash (\), w wyrażeniach wpisywanych do kodu należy go podwoić:

  1. boost::regex exp("\\w+"); // kompiluje wyrażenie \w+

W C# i Pythonie można tego uniknąć, stosując mechanizm surowych napisów (raw strings). Programiści C++ i Javy nie mają niestety tego szczęścia ;)

Gdy mamy już obiekt skompilowanego wyrażenia, możemy użyć go do jakichś pożytecznych celów. Jeśli są one proste – jak choćby sprawdzenie, czy jakiś ciąg ma formę określoną regeksem – to możemy zazwyczaj obejść się jednym prostym wywołaniem:

  1. IPV4_REGEX = re.compile(r"^([12]?\d{1,2}\.){3}[12]?\d{1,2}$")
  2. def is_ipv4_addr(text):
  3.     return bool(IPV4_REGEX.match(text))

Bardziej skomplikowane jest wyszukiwanie wszystkich dopasowań wyrażenia w danym tekście, zwłaszcza jeśli przy okazji chcemy dobrać się do fragmentów znalezionych podciągów. Tutaj zaczynają objawiać się pewne różnice między poszczególnymi językami, ale ogólny schemat pozostaje ten sam. Opiera się on na skonstruowaniu odpowiedniej pętli przelatującej po kolejnych dopasowaniach i operowaniu na obiekcie, który takie dopasowanie (match) reprezentuje:

Obiekt dopasowania udostępnia zazwyczaj kilka przydatnych metod i właściwości, jak choćby zakres indeksów znalezionego ciągu. Są też tam fragmenty, które “wpadły” w podgrupy strukturalne (subsequences, subgroups, capture groups, itp.), na które nasze wyrażenie było podzielone. Chodzi tu o jego części ujęte w nawiasy okrągłe; to, jakie konkretne znaki zostały dopasowane do każdego z nich zostaje bowiem zapamiętane w obiekcie match.
Między innymi dzięki temu faktowi możliwe jest określanie bardzo ogólnych wzorców do wyszukania w tekście, a następnie przeglądanie tego, co udało nam się znaleźć i podejmowanie decyzji na podstawie jakichś znaczących elementów dopasowania. W ten sposób możemy przetwarzać teksty o stopniu skomplikowania znacznie przekraczającym to, co w teorii daje się opisać wyrażeniami regularnymi. Żeby nie pozostać gołosłownym, zaprezentuję na przykład prosty sposób na konwersję tekstu zawierającego często spotykane na forach znaczniki BBCode (takie jak [url] czy [img]) na jego odpowiednik HTML-owy, gotowy do wyświetlenia.

  1. import re
  2.  
  3. # wyrażenie dopasowujące tagi BBCode, np. [b]foo[/b]
  4. BBTAG_RE = re.compile(r"\[\s*(\w+)\s*\](.*)\[/\s*\1\s*\]")
  5.  
  6. # funkcja zamieniająca pojedynczy tag BBCode na HTML
  7. SIMPLE_BBTAGS = { 'b': 'strong', 'i': 'em', 'u': 'u' }
  8. def _bbtag_to_html(match):
  9.     tag = match.group(1).lower()
  10.     content = match.group(2)
  11.      
  12.     if tag in SIMPLE_BBTAGS.keys():
  13.         html_tag = SIMPLE_BBTAGS[tag]
  14.         return "<%s>%s</%s>" % (html_tag, content, html_tag)
  15.     if tag == 'url':
  16.         return '<a href="%s">%s</a>' % (content, content)
  17.     if tag == 'img':
  18.         return '<img src="%s" alt="">' % content
  19.      
  20.     return ""
  21.  
  22. # właściwa funkcja
  23. def bbcode_to_html(text):
  24.     return BBTAG_RE.sub(_bbtag_to_html, text)

Najważniejsza jego część to wykonywane w funkcji _bbtag_to_html przetwarzanie obiektu typu re.MatchObject zawierającego dane o znalezionym, pojedynczym tagu. Pobieramy tam jego nazwę i zawartość, które zostały dopasowane jako odpowiednio: pierwsza i druga podgrupa wyrażenia. Samo przeglądanie tekstu w poszukiwaniu tagów i ich zastępowanie jest wykonywane wbudowaną funkcją re.RegexObject.sub, która ukrywa szczegóły wspomnianej wcześniej pętli.

Mam nadzieję, że powyższy przykład dowodzi, że możliwe jest zastosowanie wyrażeń regularnych bez znaczącego wzrostu liczby problemów do rozwiązania :) Jakkolwiek dziwnie to zabrzmi, korzystanie z regeksów może bowiem niekiedy przyczynić się do wzrostu czytelności wynikowego kodu, przynajmniej dla bardziej doświadczonych programistów. Jest tak ze względu na duże podobieństwa nie tylko między różnymi wariantami składni wyrażeń, ale też między bibliotekami do ich obsługi w różnych językach programowania, które to dzisiaj starałem się przedstawić.

Tags: , , , , ,
Author: Xion, posted under Programming » 2 comments

Importy niezupełnie z zagranicy

2011-03-17 23:55

Przeglądając plik źródłowy programu w dowolnym niemal języku, gdzieś bardzo blisko początku znajdziemy zawsze region z importami. Niekoniecznie będą one oznaczone słowem kluczowym import – czasem to będzie using, być może do spółki z #include – ale zawsze będą robiły zasadniczo to samo. Chodzi o poinformowanie kompilatora lub interpretera, że w tym pliku z kodem używamy takich-a-takich funkcji/klas/itp. z takich-a-takich modułów/pakietów. Dzięki temu “obce” nazwy użyte w dalszej części będą mogły być połączone z symbolami zdefiniowanymi gdzie indziej.

Każdy import w jakiś sposób rozszerza więc przestrzeń nazw danego modułu i zazwyczaj wszystko jest w porządku, dopóki dokładnie wiemy, jak to robi. Dlatego też powszechnie niezalecane są “dzikie” importy (wild imports), które nie wyliczają jawnie wszystkich dodawanych nazw, zwykle ukrywając je za gwiazdką (*). Ale nawet jeśli ich nie używamy, to nie oznacza to, że żadne problemy z importowanymi nazwami nas nie spotkają. Oto kilka innych potencjalnych źródeł kłopotów:

  • Importowanie nazwy, która jest identyczna z jednym z symboli zdefiniowanych w tym samym pakiecie. Import będzie wtedy ją przesłaniał – ale oczywiście tylko wtedy, jeśli go rzeczywiście dodamy. A to nie jest takie pewne zwłaszcza w języku interpretowanym, gdzie nawet symbol o zupełnie innej semantyce może łatwo przejść niezauważony aż do momentu uruchomienia kodu z jego błędnym wystąpieniem. Możemy więc nieświadomie używać nazwy lokalnej tam, gdzie należałoby raczej zaimportować zewnętrzną – lub odwrotnie.
  • Zaimportowane nazwy mogą być kwalifikowane lub nie, zależnie od języka i/lub sposobu importowania. I tak chociażby fraza import foo.bar.baz; wprowadza do przestrzeni modułu nazwę baz (czyli niekwalifikowaną) w przypadku Javy. W przypadku Pythona ten sam efekt wymaga z kolei instrukcji from foo.bar import baz, a zwykła instrukcja import da nam jedynie kwalifikowaną nazwę foo.bar.baz – która z kolei w Javie i C# jest dostępna bez żadnych importów, a w C++ po dodaniu dyrektywy #include… Całkiem intuicyjne, czyż nie? ;-) Skoro tak, to dodajmy do tego jeszcze fakt, iż…
  • Nazwy importowane można w większości języków aliasować. Oznacza to, że wynikowa nazwa wprowadzona do przestrzeni może być zupełnie inna niż ta, która jest zdefiniowana w źródłowym module. Aliasowania używa się najczęściej do skracania prefiksów nazw kwalifikowanych i choć ułatwiają one ich wpisywanie, to w rezultacie ogólna czytelność kodu może się pogorszyć.
  • W wielu językach importy mają zasięg. Przekłada się on potem na zasięg zaimportowanych nazw, którego ograniczenie jest często dobrym pomysłem. Sam import występujący poza początkiem pliku może jednak mieć niepożądane efekty – jak choćby załadowanie do pamięci importowanego modułu dopiero w momencie jego pierwszego użycia, co może wiązać się z potencjalnie dużym, chwilowym spadkiem wydajności.
  • Jeśli nie jesteśmy uważni, możemy rozpropagować zaimportowane nazwy do kolejnych modułów, które bynajmniej wcale z nich nie korzystają. Chyba najbardziej znanym przykładem jest wstawianie deklaracji using w plikach nagłówkowych C++, ale to nie jedyny przypadek i nie jedyny język, w którym importy z jednego modułu mogą zaśmiecić przestrzeń nazw innego.

Podsumowując, importy – chociaż często zarządzane prawie całkowicie przez IDE – to w sumie dość poważna sprawa i warto zwrócić na nie uwagę przynajmniej od czasu do czasu.

Tags: , , , , , , ,
Author: Xion, posted under Programming » Comments Off on Importy niezupełnie z zagranicy

Formatowanie tekstu

2010-12-12 19:03

Tematem budowania łańcuchów znaków zajmowałem się już kiedyś, badając efektywność tej operacji w C++ dla trzech różnych sposobów. Tym razem przyjrzę się sprawie z perspektywy bardziej niezależnej od języka. Ostatnio poczyniłem bowiem pewną obserwację. Zauważyłem mianowicie, że “tradycyjne” podejście do składania stringów – przez konkatenację kolejnych podciągów – staje się… hmm… niemodne, jeśli można tu użyć takiego określenia.

Ci, którzy kodują nieco dłużej niż ja pewnie zwrócą mi zaraz uwagę, że takie naprawdę tradycyjne podejście do problemu to to, które implementują funkcje z języka C podobne do printf:

  1. fprintf (stderr, "[%s] Error (errno=%d)\n", tag, errno);

Polega ono na uprzednim zdefiniowaniu wzorca tekstu (tzw. formatu, tutaj jest to drugi argument fprintf), zawierającego symbole zastępcze takie jak %s czy %d. Są one następnie zastępowane podanymi wartościami (tutaj: tag oraz errno) w celu wyprodukowania finalnego ciągu. Nie ma tu jawnej operacji łączenia dwóch napisów. Zamiast tego mały parser przegląda podany wzorzec i w miejscu symboli zastępczych wstawia do wyniku podane wartości, przepisując bez zmian resztę znaków.
Wkrótce jednak nadszedł C++ z klasami string i ostream, oferującymi podobne możliwości bez użycia wzorców. Zamiast tego posługiwały się operatorami + i << w celu wyprodukowania tekstowego wyjścia:
cerr << "[" << tag << "] Error (errno=" << errno << ")" << endl;[/cpp] To działa, ale - jak można zobaczyć w porównaniu - wygląda często znacznie mniej przejrzyście. Nie dziwi więc, że w tym względzie historia zdaje się zatoczyć koło. Obecnie bowiem operacja formatowania napisów (w sensie powyższej funkcji fprintf, a nie określania czcionki czy stylu tekstu) jest wbudowana w wiele nowoczesnych, popularnych języków programowania, jak choćby C#, Pythona czy Javę. Najczęściej też, co ciekawe, trzyma się ona z grubsza oryginalnej składni wzorców pochodzącej jeszcze z C, dodając oczywiście jakieś własne, bardziej zaawansowane rozszerzenia:

  1. exc_type, exc, _ = sys.exc_info()
  2. logging.error("[%s] %s" , exc_type.__name__, exc)
  1. Console.WriteLine("[{0}] {1}", e.GetType().Name, e.Message);

Czasami jednak – jak widać wyżej – tak nie jest :) Na obecność mechanizmu formatowania łańcuchów znaków możemy aczkolwiek liczyć w każdym szanującym się języku. I to łącznie z C++, gdzie wszystko prawie wszystko, czego w nim pierwotnie nie ma, zawiera się w jakiejś bibliotece boost – w tym przypadku jest to boost::format.

Tags: , , , ,
Author: Xion, posted under Programming » 5 comments

Zabezpieczenie przed NULL-em

2010-09-18 22:36

Defensywne programowanie wymaga, by zabezpieczać się przed różnymi niepożądanymi sytuacjami. Jedną z częstszych jest próba odwołania się do obiektu czy wartości, która nie istnieje – czyli np. dereferencja wskaźnika pustego czy użycie odwołania zawierającego null. Stąd bardzo częste ify w rodzaju:

  1. if (!p) return false;
  1. if (arg == null) throw new ArgumentNullException("arg");

Kiedy jednak sprawdzenie nullowatości jest tylko częścią warunku, wtedy w ruch idzie zwykle “sztuczka” z leniwą ewaluacją (lazy evaluation):

  1. if (p && p->key == k) return p;

Większość języków ją dopuszcza, jako że jest ona przy okazji pewną formą optymalizacji. Jeśli bowiem pierwszy argument operatora logicznego daje informację o prawdziwości/fałszywości całego wyrażenia, nie trzeba już wyliczać drugiego.

Inną typową sytuacją jest zamiana nulla na jakąś inną wartość domyślną:

  1. string str = s != null ? s : "N/A"; // C++/C#/Java
  1. s = s if s != None else "N/A" # Python

Tutaj C# oferuje specjalny operator ??, pomyślany właśnie na tego typu okazje:

  1. string str = s ?? "N/A";

Działa on dobrze z typami Nullable, czyli specyficznym rodzajem typów pochodnych, które dopuszczają wartość null tam, gdzie typ macierzysty jej nie przewiduje:

  1. int? x = null; // liczba typu int lub null
  2. int y = x ?? -1; // ustaw y = -1; jeśli x == null

Operator ?? przydaje się wtedy do konwersji na typ bazowy z określoną wartością domyślną.

Tags: , , ,
Author: Xion, posted under Programming » Comments Off on Zabezpieczenie przed NULL-em
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.