11/03/2011

Kilka słów o operatorach

Home

Dzisiaj kolega podesłał mi swoistą zagadkę w postaci dwóch, pozornie robiących to samo, fragmentów kodu. Jeden napisany w C#, a drugi w VB.NET:
int counter = 0;
while (++counter < 10)
{
  Console.WriteLine(counter.ToString());
}
Dim counter As Integer = 0
While (++counter < 10)
  Console.WriteLine(counter.ToString())
End While
Różnica polega na tym, że pętla w C# wykona się 9 razy, a w VB.NET otrzymamy pętlę nieskończoną. Dlaczego? Przyjrzyjmy się dokładniej pętli napisanej w języku VB.NET. Ci z was, którzy programują lub programowali w tym języku zapewne zwrócili uwagę na użycia operatora preinkrementacji. No właśnie, od kiedy w VB.NET mamy operator preinkrementacji? I tu tkwi pies pogrzebany, w VB.NET nie ma operatora preinkrementacji czy też predekrementacji. W takim razie czemu ten kod w ogóle się kompiluje? Początkowo pomyślałem, że to błąd kompilatora. W przekonaniu tym utwierdził mnie dodatkowo reflector, który po wrzuceniu do niego dll'ki pokazał coś takiego:
...
Do While (counter < 10)
...
Efekt zaobserwowałem w VS 2010, a więc eksperyment przeprowadziłem jeszcze w VS 2008 z tym samym wynikiem. W tym momencie pomyślałem, że to jednak niemożliwe aby przez tyle lat taki bug pozostał niewykryty. Po chwili mnie olśniło i wszystko okazało się proste i oczywiste. Zanim rozwiążę zagadkę do końca powiem, że poniższy kod również się skompiluje:
Dim counter As Integer = 0

While (++-+---++--+-+-counter < 10)
  Console.WriteLine(counter.ToString())
End While
C# nie jest gorszy i w nim również da się stworzyć podobnego dziwoląga:
int counter = 0;
while (+-+-+-+-+--counter < 10)
{
  Console.WriteLine(counter.ToString());
}
W tej chwili wszystko powinno być już jasne. + i - to nie tylko operatory binarne ale również operatory unarne. Poniżej fragment dokumentacji z MSDN:

Unary + operators are predefined for all numeric types. The result of a unary + operation on a numeric type is just the value of the operand.

Unary - operators are predefined for all numeric types. The result of a unary - operation on a numeric type is the numeric negation of the operand.

W języku VB.NET, tak jak pisałem wcześniej, nie ma operatorów preinkrementacji oraz predekrementacji, a więc zmienną liczbową możemy poprzedzić dowolną kombinacją + i -. W języku C# takie operatory istnieją, a więc zmienna liczbowa może zostać poprzedzona ciągiem + i - pod warunkiem, że występują one naprzemiennie (z wyjątkiem ostatniej pary).


26/02/2011

Jak zagłodzić Timer?

Home

Okazuje się, że bardzo prosto, ale zacznijmy od początku. Niedawno zakończyłem pracę nad serwerem zajmującym się wykonywaniem tzw. zadań wsadowych. Definicje zadań do wykonania pobierane są z bazy danych, a w danym momencie może działać wiele serwerów. Każdy serwer rezerwuje sobie swoje zadania na pewien kwant czasu. Po upływie tego czasu inne serwery mają prawo przejąć to zadanie. Może się tak zdarzyć na przykład jeśli jakiś serwer ulegnie awarii. Jeśli wykonanie danego zadania zajmuje więcej czasu niż czas rezerwacji to serwer musi przedłużyć dzierżawę.

W tym celu stworzyłem komponent, który monitoruje zadania przetwarzane przez serwer i kiedy zbliża się czas wygaśnięcia dzierżawy, przedłuża ją. Komponent ten korzysta z klasy Timer z przestrzeni nazw System.Timers, która co określony kwant czasu generuje zdarzenie. W metodzie obsługującej to zdarzenia nie robię nic innego jak po prostu aktualizuję czas wygaśnięcia dzierżawy. Tyle w telegraficznym skrócie.

W czasie testowania stworzonego rozwiązania zauważyłem, że w niektórych przypadkach czas wygaśnięcia dzierżawy nie jest aktualizowany. Wyglądało na to jakby klasa Timer nie generowała zdarzeń albo generowała je z dużym opóźnieniem! Wbrew pozorom rozwiązanie zagadki okazało się bardzo proste. Otóż klasa Timer generuje zdarzenie Elapse w wątku pochodzącym z puli wątków ThreadPool. Jeśli dodamy do tego fakt, że zadania wsadowe również są wykonywane przez wątki z puli to wszystko staje się oczywiste. Jeśli serwer umieści w puli odpowiednio dużo zadań wsadowych to może okazać sie, że brakuje wątków dla klasy Timer.

Poniżej prosty kod prezentujący ten efekt. Na początku ograniczam liczbę wątków do 10 i zlecam wykonanie 10 zadań. Zanim wszystkie zadania zostaną uruchomione na ekran zostanie wypisanych kilka napisów Hello World from System.Timers.Timer!. Następnie kiedy wszystkie 10 zadań zostanie uruchomionych napis przestanie się pokazywać. Zobaczymy go ponownie kiedy przynajmniej jedno zadanie zostanie zakończone i tym samym zwolni sie jeden wątek.
class Program
{
  static void Main(string[] args)
  {
    ThreadPool.SetMaxThreads(10, 10);
    
    System.Timers.Timer t = new System.Timers.Timer();
    t.Interval = 1000;
    t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed);
    t.Start();

    for (int i = 0; i < 10; ++i)
        ThreadPool.QueueUserWorkItem((o) =>
            {
              Console.WriteLine("Start " + o);
              Thread.Sleep(10000);
              Console.WriteLine("End " + o);
            }, i);

    Console.ReadLine();
  }

  static void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
    Console.WriteLine("Hello World from System.Timers.Timer!");
  }
}
Taki sam efekt otrzymamy jeśli użyjemy klasy System.Threading.Timer zamiast System.Timers.Timer. Jak można poradzić sobie z tym problemem. Uważam, że są trzy rozwiązania:
  • Zliczać liczbę zadań umieszczanych w puli wątków i pilnować aby zawsze, przynajmniej jeden wątek z puli był wolny i mógł być użyty przez klasę Timer. Maksymalną liczbę wątków możemy pobrać przy pomocy metody GetMaxThreads. Jest to jednak podejście sprzeczne z ideą puli wątków, do której wrzucamy zadania do wykonania i nie zastanawiamy się kiedy dokładnie zostanie uruchomione, przez jaki wątek itd.
  • Zrezygnować z klasy ThreadPool i samemu zarządzać wątkami.
  • Użyć (napisać) zegar odpalający zdarzenia Elapse poza pulą wątków.
W opisie problemu pominąłem Task Parallel Library ponieważ w omawianym przypadku użycie .NET 4.0 nie było możliwe. Szybki test pokazał jednak, że jeśli mógłbym użyć Task Parallel Library to problem nie wystąpiłby. Z tego co wiem Task Parallel Library nie używa klasy ThreadPool, a więc przyczyna problemu nie występuje.

21/02/2011

Domeny globalne

Home

Od kilku miesięcy jestem szczęśliwym posiadaczem własnej domeny michalkomorowski.com. Jakiś czas temu moje szczęście zostało jednak zakłócone kiedy dowiedziałem się, że usługa whois udostępnia publiczne moje pełne dane adresowe!!! Szybko skontaktowałem się z firmą home.pl, za pośrednictwem której wykupiłem domenę aby wyjaśnić sprawę.

Co tu dużo mówić byłem nastawiony wojowniczo ale mój zapał został szybko zgaszony. Otóż okazało się, że dla domen globalnych dane właścicieli są domyślnie jawne. Wymaganie takie stawia rejestratorom organizacja ICANN czyli główny zarządca domen na świecie. W regulaminie znajduje się oczywiście stosowny punkt, który przeoczyłem przeglądając umowę. Szczęście w nieszczęściu aby ukryć dane wystarczy wypełnić elektroniczny formularz. Po jego wypełnieniu moje dane zostały ukryte w ciągu 24 godzin, tutaj plus dla home.pl za szybką reakcję.

Z drugiej strony sądzę, że w dobrym tonie byłoby gdyby firmy zajmujące się rejestracją domen informowały o takich rzeczach przy pomocy dużego czerwonego napisu ponieważ sprawa nie jest oczywista. Z ciekawości sprawdziłem kilka domen globalnych dla polskich blogów i dla większości z nich wynik zwrócony przez usługę whois, zawierał dane adresowe właścicieli. Może sie mylę ale z dużą pewnością ich autorzy oraz wielu innych nie zdaje sobie z tego sprawy.

16/02/2011

DockPanel i ScrollViewer

Home

Dlaczego lubię kontener DockPanel? Ponieważ dobrze (intuicyjnie) współpracuje z kontrolką ScrollViewer, która dostarcza pionowych i poziomych pasków przewijania. Załóżmy, że główne okno naszej aplikacji zawiera listę (kontrolka ListView) z kilkuset wierszami. Wierszy jest na tyle dużo, że w danym momencie na ekranie widoczna jest tylko część z nich. Aby użytkownik mógł przewijać listę i zobaczyć wszystkie wiersze używamy kontrolki ScrollViewer

ScrollViewer zachowa się jednak inaczej w zależności od kontenera w jakim zostanie osadzony. Jeśli będzie to np.: StackPanel lub Grid to paski przewijania nie zostaną wyświetlone i otrzymamy efekt jak poniżej.



Można sobie z tym poradzić ustawiając na kontrolce ScrollViewer właściwość Height ale spowoduje to, że lista będzie miała zawsze tą samą wysokość bez względu na wielkość okna. Podobnie można ustawić właściwość Height dla listy ale efekt będzie taki sam.



Dużo lepszym rozwiązaniem jest osadzenia kontrolki ScrollViewer wewnątrz kontenera DockPanel. Dzięki temu uzyskamy efekt jak poniżej. Paski przewijania są widoczne, a wysokość listy zależy od wielkości okna.


29/01/2011

Być czy nie być, pisać czy nie pisać komentarze?

Home

Programiści dzielą się na:
  • Tych, którzy piszą bardzo dużo komentarzy, czasami prawie w każdej linijce. Tych spotkałem niewielu.
  • Tych, którzy w ogóle ich nie piszą, nawet jeśli napisali kod, którego nie da się zrozumieć bez choćby odrobiny komentarza. Tych ortodoksów jest już więcej.
  • Największa grupę stanowią natomiast programiści, którzy sytuują się gdzieś pomiędzy tymi dwiema skrajnościami.
Kiedy zaczynałem programować pisałem bardzo dużo komentarzy. Wiele z nich było zupełnie niepotrzebnych, na przykład komentarz "Konstruktor domyślny" dla domyślnego, bezparametrowego konstruktora. Z biegiem lat, kiedy nabrałem doświadczenia przesunąłem się gdzieś w okolicę środka na skali pomiędzy ortodoksyjnymi zwolennikami komentarzy i ortodoksyjnymi przeciwnikami komentarzy. Wciąż bliżej mi jednak do tych pierwszych.

Jednak, ostatnio dwa zdarzenia spowodowały, że częściowo przewartościowałem swój sposób patrzenia na tę sprawę i moje nastawienie przesunęło się znacząco w kierunki nie pisania komentarzy. Co to za zdarzenia? Po pierwsze postanowiłem wrócić do swojego starego projektu napisanego w C# ponad 3 lata temu. W skrócie program realizuje wnioskowanie wstecz. Właściwy silnik wnioskujący ma około 800 linii kodu (nie licząc GUI i definicji prostych struktur danych). Przy czym ponad połowa z tego przypada na komentarze, definicje regionów i znaki nowe linii! Problem w tym, że pomimo dużej liczby komentarzy miałem dużą trudność w zrozumieniu tego kodu i niektórych rozwiązań jakie przyjąłem. Drugie zdarzenie dotyczyło cudzego kodu, w którym skomentowana była prawie każda linijka. Początkowo myślałem, że ułatwi to sprawę ale myliłem się. Tak duża liczba komentarzy skutecznie utrudniała zrozumienie kodu. Usunięcie znaczącej części tych komentarzy (i refactoring) sprawiło, że kod stał się dużo łatwiejszy w odbiorze.

W obecnej chwili uważam, że większe znaczenie ma odpowiedni podział kodu na klasy, a przede wszystkim na metody. Zamiast komentarza lepiej zamknąć kod, którego dotyczy w metodę o samo tłumaczącej się nazwie. Komentarze w kodzie staram się ograniczyć do miejsc, które rzeczywiście tego wymagają: skomplikowany algorytm, szybko wprowadzane poprawki itd. Jeśli chodzi o komentarze do metod to uważam, że są one niezbędne tam gdzie z nazwy metody i nazw jej parametrów nie można wywnioskować jej przeznaczenia. Jeśli mamy skłonność to niepisania komentarzy to warto spytać innego programisty co jego zdaniem robi dana metoda. Jeśli odpowie poprawnie to komentarz z dużym prawdopodobieństwem nie jest potrzebny. Komentarz jest również potrzebny jeśli metoda rzuca jakieś wyjątki. Co jednak z metodami publicznymi i chronionymi czyli tymi, z których potencjalnie będą korzystać inni. Tutaj mam mieszane uczucia. Z jednej strony dobra praktyka mówi, że dla tych metod komentarze są potrzebne. Z drugiej po co komentować metodę o takim nagłowku IList<User> GetAllUsersWithBankAccount(), która zwraca listę użytkowników z kontem bankowym?