środa, 13 czerwca 2012

przeciążąnie GetHashCode

Doszukałem się dziś ciekawego jak dla mnie posta.

metoda T.Equals(T objToCompare) porównuje 2 instancje obiektów, zwracając boolowską wartość, czy są sobie równe czy nie. Przeciąża się to we własnych klasach, aby umożliwić rozmaitym IComparerom porównywanie naszych obiektów. To wiedzą wszyscy.

Metoda ta należy do typu object, więc każda instancja obiektu ma tą metodę.

Nieco większą zagadką jest metoda GetHashCode.
Co prawda visual studio rzuca nam warningiem, jeżeli przeciążymy metodę Equals, nie przeciążąjąc metody GetHashCode. Nie wiem jak inni, ja się w ten warning nie zagłębiałem. Po nazwie metody wywnioskowałem że zwraca ona hash do tablic hashujących czyli np Dictionary. W końcu porównywanie hashy jest zawsze szybsze niż porównywanie całych obiektów, to to nawet na studiach było na 2 roku na AiSD.

A jakoś nie zdarzyło mi się jeszcze używać w Dictionary własnych obiektów które musiałbym porównywać między sobą operatorem Equals() lub ==, więc jakoś sobie radziłem z brakiem tej wiedzy.

Tymczasem dziś przypadkowo znalazłem:

"Dlaczego przeciążąnie GetHashCode() jest ważne? Jeżeli Hash zwrócony przez GetHashCode dla dwóch różnych obiektów nie będzie sobie równy, te obiekty nigdy nie zostaną uznane za równe sobie, i metoda Equals() nie zostanie dla nich uruchomiona nigdy. Powinieneś w pierwszej kolejności przeciążyć GetHashCode() i zwracać ten sam hash dla dwóch równych sobie obiektów".
No proszę.. .NET już domyślnie wykorzystuje ten fajny algorytm o którym się uczyliśmy tak dawno, porównując najpierw hashe obiektów, a dopiero wtedy gdy się zgadzają - uruchamiając funkcję compare (bo przecież może być kolizja hashy i nie można na samych hashach w 100% polegać).

Teraz nawet sobie przypominam że dziwiłem się czego Equals() nie działa w jakims swoim małym programiku.. ale zamiast wgłębiać się w przyczyny, średnio był czas na to przy "nieciekawym" małym programiku, po prostu napisałem porównanie w metodzie "na sztywno" i też działało. I zapomniałem o sprawie.

Dobrze że na to trafiłem, człowiek uczy się całe życie, tym bardziej tak rozbudowanego środowiska jakim jest .NET. Czasem robiąc "ciekawsze" rzeczy, można zapomnieć o podstawach :)


Skoro już jesteśmy przy liczeniu hashy, podzielę się z wami tym co wyniosłem z zabawnego wykładu AiSD który w całości był dyktowany z pożółkłej już ze starości kartki :) (Niech żyje S....! Niech żyje P...!)

Metoda GetHashCode() powinna być raczej prosta i szybka w wykonywaniu niż bardzo dokładna (w sensie rzadko generująca kolizję). Jej celem jest odsianie wstępne większości obiektów, które na pewno nie będą sobie równe z racji róźnych hashy, kolizje i tak zostaną zweryfikowane metodą Equals. Chodzi tu o szybkość porównywania, więc lepiej jeżeli metoda GetHashCode generuje częstsze kolizje (np w 10% przypadków) ale będzie 10x szybsza niż jeżeli wygeneruje kolizje w przypadku 1%, ale będzie 10x wolniejsza. Jeżeli już uruchomi się metoda Equals, w której np. sprawdzimy wszystkie pola klasy metodami Equals, to dla nich też, zanim zostanie uruchomione Equals, zostanie najpierw uruchomione ich GetHashCode, więc ta metoda Equals wcale nie musi być dużo wolniejsza od samego GetHashCode. Może kiedyś wróce do zagadnienia (jak nie będe miał na głowie 7 sprawozdań na jutro), i machnę mały teścik z różnymi strategiami tego, z odpowiedzią która jest najwydajniejsza.

Choć i tak w 90% przypadków wydajność nie będzie miała żadnego znaczenia - a jeśli już bierzemy sie za przetwarzanie dużych zbiorów danych, dlaczego robić to w kodzie a nie użyć do tego np. bazy danych SQL?

Częste kolizje w GetHashCode() mogą jedynie popsuc wydajność w hashowanych kolekcjach (HashMap, HashSet), jeżeli twoja klasa nie bedzie jakoś intensywnie używana w tych kolekcjach, to lepiej zrobić szybszą GetHashCode().

Wg mnie najprostszą metodą na GetHashCode i zapewne wcale nie najgorszą będzie po prostu dodanie do siebie wartości zwróconej przez metodę GetHashCode dla wszystkich pól naszej klasy, i pomnożenie każdej z nich przez inną liczbę pierwszą. To wygeneruje dużo kolizji, ale będzie bardzo szybkie.

Innym sposobem jest hashGenerator wbudowany we framework. Podobno ten generator jest bardzo dobrej jakości (mało kolizji). I nie dublujemy kodu.
return new { A = PropA, B = PropB, C = PropC, D = PropD }.GetHashCode();
Podstawiamy do anonimowego obiektu pola naszej klasy, i uruchamiamy metode GetHashCode. jednak obawiam się że tworzenie za kazdym razem nowego obiektu może nie być wcale takie szybkie jakbyśmy tego oczekiwali. Jednak dla miejsc niekrytycznych dla wydajności kodu, taka implementacja będzie w pełni wystarczająca.

Należy również pamiętać że wiele typów w .NET (również tych wbudowanych) nie gwarantuje że hashe będą stałe pomiędzy kolejnymi uruchomieniami aplikacji. Identyczny obiekt wczytany w 2 różnych instancjach aplikacji prawdopodobnie będzie miał różny hash, więc do porównywania obiektów pomiędzy instancjami trzeba zrobić jakieś dodatkowe ID, na pewno nie powinno się używać do tego hashy z tej metody.

poniedziałek, 11 czerwca 2012

Boje z ASP.NET MVC AJAX i pragma no-cache

Dziś napotkaliśmy w pracy dość ciekawy problem który nie dał się rozwiązać szybko na pierwszy rzut oka.

Pisząc portal w ASP.NET MVC, używając widoków Razor, postanowiłem zdynamizować nieco stronę główną, i zastąpić linki generowane przez helper @Url.Action, linkami pochodzącymi z helpera @Ajax.ActionLink.

Linki te to taka bezpieczna ajaxowa nakładka na nawigację - jest to pełnoprawny, normalny link, który jest indeksowalny dla wyszukiwarek, jednak jeżeli użytkownik ma włączoną obsługę javaScriptu, wtedy kliknięcie w link spowoduje wejście na url linka za pomocą Ajax, i wyświetlenie zawartości w obiekcie o zadanym ID.

Używam tych linków w kluczowych miejscach portalu którym aktualnie się zajmuję - w części w której większość użytkowników spędzi dużo czasu, więc warto zainwestować w ergonomię i pozwolić użytkownikom np. na przeglądanie newsów bez nieco deprymującego odświeżenia strony w przeglądarce.

Ponieważ Ajax.ActionLink prowadzi pod ten sam adres URL, co jego nieajaxowa wersja, ASP.NET posiada metodę obiektu Request dostępnego w każdym kontrolerze i widoku: Request.IsAjaxRequest(). Jeśli ta metoda zwróci true, to oznacza to że widok został użyty w akcji kontrolera która nie została załadowana zwykłym GET-em, tylko właśnie za pomocą ajax.

Dzięki temu, możemy zadecydować, czy widok ma zwrócić całą stronę, wraz z jej parent layoutem, czy tylko samą treść, ponieważ zapytanie jest ładowane przez ajax, i parent layout już istnieje.

przykładowy widok, który może być odczytywany w dwójnasób: zwykłym linkiem GET, oraz ajaxem.


@{
ViewBag.DontDrawMainPagePiece = true;
ViewBag.Title = _LayoutResources.Actuals;
if (Request.IsAjaxRequest() == false)
{
Layout = "~/Views/Shared/Layouts/AjaxLayout.cshtml";
}
else { Layout = null; }
}
@Html.Action( "NewsListPartial", new { id = ViewBag.PageId } )





natomiast AjaxLayout.cshtml zawiera coś takiego


[......]

<div class="content_container" id="ajax_container">
@RenderBody()
</div>
[......]


przykładowy ajax link który działa jako get na stronie bez JS i jako ajax w pełni kompatybilnej przeglądarce, współpracujący z tym wygląda tak:

@{var updateTargetId="ajax_container";}

@Ajax.ActionLink( "link_text", Model.ActionName, Model.ControllerName,
Model.RouteParameters, new AjaxOptions
{
LoadingElementId = "loader_image",
UpdateTargetId = updateTargetId,
OnBegin = String.Format("fadeOut('{0}')",updateTargetId),
OnComplete = String.Format("fadeIn('{0}')", updateTargetId),
HttpMethod = "Post"
} )

taki piękny link, wygeneruje nam coś co odwoła się do akcji z modelu, natomiast wszystko co ten ajax request zwróci, zostanie załadowane do obiektu DOM o id #ajax_container. Dlatego ajax_container jest na zewnatrz, w pliku AjaxLayout.cshtml - jezeli strona jest odczytywana po get - musi zostac zrenderowana z calosci razem z otaczającym treść htmlem. treść znajduje sie w divie o id #ajax_container.

Jesli klikniemy w ajaxlink, to to co zostanie zwrocone zostanie załadowane do diva ajax_container.
Poniewaz jest to request ajaxowy, a w widoku zawarliśmy stwierdzenie że, jeżeli request jest ajaxowy, to layout= null, zostanie zwrócona sama treść, bez kolejnego ajax_container, dzięki czemu przegladarka nie zglupieje. Wszystko sie zgadza, layout pozostal bez zmian, zmieniła się sama treść.


No i tak zrobiliśmy. Po publishu sie okazało że strona rozkraczyła się pod Internet Explorerem 9. Każda przeglądarka interpretowala link poprawnie, i wyświetlała treść poprawnie, natomiast internet Explorer umieszczał w ajax_container, zrenderowaną w całości strone, wraz z otaczającym ją layoutem, strona w stronie, menu 2 razy, efekt opłakany. Pod każą inną przeglądarką wszystko działało poprawnie. So confusing... błąd jest ewidentnie po stronie serwera więc jak może go powodować inna przeglądarka?

Sprawdziliśmy snifferem Fiddler co się dzieje na łączach. Okazało się że z jakiegoś powodu prawie wszystkie akcje w responsach zwracają pragma: no-cache, jednak strona główna z jakiegoś powodu nie. I ta drobna różnica stała się przyczyną błędu i zamieszania.

Uruchomiłem debugger, rozstawiłem breakpointy. Internet explorer wykonywał request na Home/Index, ten go przekierowywał do właściwej strony, jednak, on miał już u siebie w cache skeszowaną wersję tej strony, pobraną przy pierwszym wejściu na stronę główną (za pomocą GET), wraz z całym layoutem (choć tym razem request był ajaxowy, i pomimo że ASP.NET, już się wziął za ponowne zrenderowanie tego widoku (tym razem bez tej całej otoczki zawartej w ajaxLayout.cshtml), Internet explorer zwrócił stronę z cache, wraz z menu, stopką, nagłówkiem...).

Natomiast np google chrome czy mozilla, ponieważ w piewszym response nie było wyraznego polecenia cache-owania danego URL-a, nie robiły tego, i strona ładowała się jak należy.

Problem wynika z tego że IE oraz reszta różnie interpretują parametr w nagłówku HTTP o nazwie pragma.
Internet explorer, gdy go nie znajdzie, to zakłada że strone trzeba scache-ować.
Natomiast reszta przegladarek zakłada że gdyby developer chcial by strona była cache-owana, to by umiescił tam pragma-cache i jakąś wartość.

powyższy kawałek kodu w global.asax, w event handlerze PreRequestExecuteHandler załatwia sprawę:

this.Response.AddHeader( "Cache-Control", "no-cache, no-store, max-age=0, must-revalidate" );

od tej pory wszystkie strony funkcjonują jednakowo we wszystkich przeglądarkach pod względem cache.