23/03/2011

Przygody ze Steam'em

Home

Shogun: Total War to jedna z pierwszych gier jaką odpaliłem na swoim pierwszym komputerze PC i zarazem jedna z najlepszych gier w jakie grałem. Bardzo podobała mi się również inna gra z tej serii czyli Rome: Total War. Ostatnio postanowiłem nadrobić zaległości i kupiłem ostatnią grę z tej serii czyli Napoleon: Total War.

Napoleon: Total War jak wiele obecnie sprzedawanych gier wymaga do działania konta w serwisie Steam. Żaden problem ponieważ takie konto posiadam już od dawna. Niestety Steam potrafi być bardzo irytujący. Zacznę od tego, że po uruchamieniu klient Steam sprawdza czy są dostępne aktualizacje i pobiera je nie pytając użytkownika o zdanie. W tej chwili dysponuję połączeniem modemowym, a więc jest to dla mnie tym bardziej uciążliwe.

Na tym nie koniec. Zanim udało mi się uruchomić właściwy proces instalowania gry napotkałem jeszcze jeden problem. Po uruchomieniu instalatora pojawił sie błąd Failed to run install script. Szczęśliwie udało mi się go szybko rozwiązać, uruchamiając proces instalacji z wiersza poleceń przy pomocy komendy steam.exe -install h: gdzie h: to napęd DVD.

Kiedy instalacja zakończyła się i myślałem, że w końcu zagram okazało się, że Steam ma jeszcze coś do powiedzenia i rozpoczął aktualizację dopiero co zainstalowanej gry. Wyłączenie automatycznej aktualizacji we właściwościach Napoleon: Total War, odłączenie Internetu, ponowne uruchomienie klienta Steam nie pomogło. W tej chwili proces instalacji zakończył się i udało mi się uruchomić grę :) Tak sobie jednak myślę, że stare dobre czasy kiedy kupowało się grę, przychodziło do domu, instalowało i grało bez potrzeby aktywowania przez Internet, przymusowej aktualizacji itp. mijają albo już minęły.

19/03/2011

Jak właściwość może napsuć krwi

Home

Właściwości to jedna z moich ulubionych cech języka C#. W poście tym nie będę jednak przekonywał opornych, jeśli tacy są, do ich używania. Opiszę natomiast przypadek złego wykorzystania właściwości, który ostatnio napsuł mi trochę krwi. Załóżmy, że mamy aplikację WWW lub grubego klienta. Aplikacja ta komunikuje się z bazą danych za pośrednictwem tzw. dostawców danych czyli klas implementujących dobrze określone interfejsy. W zależności od konfiguracji dostawca może być dostępny lokalnie w ramach procesu aplikacji lub zdalnie czyli być uruchomiony na innej maszynie np. jako usługa WCF. Z perspektywy aplikacji jest to przezroczyste.

Dla ustalenia uwagi przyjmijmy, że dostawca udostępnia metodę Customer GetCustomer(int customerId), która zwraca obiekt klasy Customer reprezentujący klienta o podanym identyfikatorze. Problem polega na tym, że w zależności od tego czy dostawca danych działa lokalnie czy zdalnie to dla tego samego identyfikatora zwraca trochę inne dane. Dokładniej mówiąc jeśli mamy do czynienia z dostawcą lokalnym wszystko jest w porządku. Natomiast dla dostawcy zdalnego zwracane dane są niekompletne w tym sensie, że nie wszystkie właściwości obiektu Customer są ustawione. Co ciekawe po zdebugowaniu procesu serwera okazuje się, że zwracane przed dostawcę dane są poprawne ale po dotarciu do aplikacji okazują się jednak niekompletne.

Początkowo przecierałem oczy ze zdziwienia bo jak to możliwe, że część danych nagle znika. Po zagłębieniu się w problem znalazłem jednak winnego, którym okazała się błędne zaimplementowana właściwość w połączeniu z serializacją XML'ową. Istotę problemu przedstawia poniższy kod:

private string _StringValue;
public string StringValue
{
  get
  {
    if(_StringValue == "aaa")
      return "Ala ma kota";
    else if(_StringValue == "bbb")
      return "Kot ma Alę";
    ...
    else
      return null;
  }
  set
  {
    _StringValue = value;
  } 
}

Jak widać wartość zwracana przez getter właściwości jest inna niż wartość ustawiana przy pomocy settera. W przypadku kiedy dostawca działa lokalnie nie stanowi to żadnego problemu ponieważ pracujemy bezpośrednio z klasą dostawcy danych. W przypadku kiedy dostawca uruchomiony jest zdalnie to w rzeczywistości po stronie aplikacji pracujemy z obiektem proxy. W momencie wywołania metody GetCustomer do serwera wysyłane jest odpowiedni komunikat i wywoływana jest "prawdziwa" metoda GetCustomer. W wyniku otrzymujemy obiekt klasy Customer, która musi zostać przesłany do proxy. W tym celu obiekt musi zostać zserializowany czyli zapisany do strumienia i wysłany przez sieć.

I tutaj jest pies pogrzebany. Jeśli zostanie użyta serializacja XML'owa czyli taka, która "zamieni" obiekt na dokument XML to do proxy zostanie wysłana wartość zwrócona przez getter właściwości czyli coś innego niż wartość, która została ustawiona przy pomocy settera np.: jeśli do ustawienia właściwości StringValue użyjemy łańcucha "aaa" to w dokumencie XML zostanie zapisana wartość "Ala ma kota".

Po odebraniu odpowiedzi proxy spróbuje odtworzyć obiekt na podstawie odebranego dokumentu XML. Do zainicjowania właściwości StringValue użyje wartości z dokumentu czyli łańcucha "Ala ma kota". Teraz jeśli odczytamy właściwości StringValue to dostaniemy null ponieważ kod w getterze nie rozpozna łańcucha "Ala ma kota". Jeśli użyjemy serializacji binarnej problem nie wystąpi ponieważ w strumieniu zostanie zapisana wartość prywatnego pola _StringValue. Jeśli chcemy pozostać przy serializacji XML'owej to należy trochę zmodyfikować kod. Właściwość StringValue powinna być tylko do odczytu. Należy również stworzyć dodatkową właściwość do ustawiania danych.

public string StringValueForSerialization
{
  get; set;
}

public string StringValue
{
  get
  {
    if(StringValueForSerialization == "aaa")
      return "Ala ma kota";
    else if(StringValueForSerialization == "bbb")
      return "Kot ma Alę";
    ...
    else
      return null;
  }
}


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).