Spis treści:

Kategoria:C#


Cache - implementacja bufora podręcznego w .NET

Implementacja cache - ikona pamięci podręcznej

Czym jest cache

W informatyce istnieje wiele różnych pojęć. Te, które istnieją, też nie zawsze istnieją pod jedną nazwą. Jeszcze trudniej jest w przypadku tłumaczeń, które, oprócz adaptacji zwykle najszybciej przyjmujących się dźwiękowych odpowiedników, dokładają swoje, słownikowe tłumaczenia. Mamy przecież angielskie cache oraz buffer, polskie cache i bufor, stosuje się również pojęcie pamięci podręcznej. Wszystkie te nazwy mogą się przeplatać i siać niepokój w głowach. Nie należy się tym martwić, bo, przyjmując odpowiednio szeroką definicję, odnoszą się do jednego. Niektórzy wprawdzie różnicują pojęcia bufora i cache, ale często przypomina mi to dyskusje o wyższości żurku nad barszczem białym i odwrotnie - dyskusje bardziej filozoficzne niż praktyczne.

Cache w informatyce to taki komponent czy też składnik większego systemu, którego zadaniem jest przechowywanie danych w celu szybszego dostarczenia ich w przypadku ponownego ich zażądania. W takiej pamięci podręcznej przechowywane są zwykle dane, których pozyskanie wymaga długich obliczeń, odwołania się do magazynu danych, którego czas odpowiedzi jest wyraźnie wolniejszy od czasu odpowiedzi magazynu wykorzystywanego przez pamięć podręczną. Dane w pamięci cache mogą być odpowiednio przekształcone w celu lepszego ich przystosowania do wykorzystywanych w systemie algorytmów (dane tabelaryczne na drzewa, dane z pliku na tablicę z haszowaniem).

Zastosowanie pamięci podręcznej

Pamięć podręczna jest bardzo często wykorzystywanym składnikiem różnych rozwiązań informatycznych. Od procesora, który wykorzystując lokalną pamięć rezygnuje z bardziej kosztownych odwołań do zewnętrznej pamięci operacyjnej, przez dysk twardy, który pobiera dane z magnetycznych talerzy (HDD) do szybszej pamięci tranzystorowej lub kondensatorowej pozwalając na szybsze odwołania do danych już tam obecnych. Cache wykorzystywany jest w przeglądarkach internetowych do przechowywania pobranych plików HTML, JavaScript, CSS i obrazków. Jeżeli przeglądarka wykryje, że użytkownik próbuje pobrać ten sam plik, pobierze go z lokalnej pamięci rezygnując ze znacznie bardziej kosztownego żądania HTTP. Pójdźmy dalej i popatrzmy na bazy danych. Tam aż roi się od różnie rozumianych pamięci podręcznych: plany zapytania (raz wyliczone przechowywane są w buforze i wykorzystywane wielokrotnie), indeksy często wykorzystywanych tabel (dostęp do pamięci operacyjnej jest szybszy niż do macierzy dyskowej) czy też same wyniki zapytań (drugie takie samo zapytanie będzie szybsze niż pierwsze). Żeby nie przedłużać listy zastosowań w nieskończoność nadmienię tylko, że pamięć podręczna wykorzystywana jest również w zwykłych aplikacjach. Wyobraźmy sobie, że dane o uprawnieniach przechowywane są w pliku lub w bazie danych, a same uprawnienia sprawdzane są przy każdej operacji - można takie dane odpowiednio przetworzyć, zbudować odpowiednie struktury i umieścić w szybkiej pamięci operacyjnej. Te często wykonywane operacje sprawdzania uprawnień nie będą powodowały zauważalnego spadku wydajności aplikacji.

Trzeba wiedzieć, że w .NET jest wiele miejsc, w których dostępny jest jakiś rodzaj pamięci podręcznej. ASP.NET ma pamięć sesji, ma nawet obiekt o nazwie Cache. Podręczną pamięcią aplikacji mogą być zwykłe pola statyczne, singletony, późne wczytywanie można zrealizować przy pomocy obiektu Lazy<T> działającego podobnie do obiektu opisanego tutaj: Opóźnione wczytywanie danych w C#.

W różnych wpisach staram się unikać prezentowania gotowych rozwiązań jednej bądź drugiej firmy. Staram się pokazać koncepcje i sposób implementacji. Nie inaczej będzie tym razem.

Programowanie obiektowe z odsieczą

Projektowanie odpowiednich klas wymaga analizy. Odpowiednie klasy powinny być niezawodne i łatwe w obsłudze. Powinny być również na tyle elastyczne, aby dało się je wykorzystywać w wielu miejscach. Te założenia przełożyły się na następujący plan:

  • wartości będą przechowywane w postaci pary klucz-wartość,
  • typ klucza i wartości może być dowolny,
  • klasa będzie silnie typowana,
  • klasa sama będzie uzupełniała brakujące rekordy,
  • dostępne będą podstawowe operacje związane z kolekcjami (dodawanie, usuwanie),
  • elementy nieużywane przez wskazany czas będą usuwane z bufora.

To są założenia wstępne. W końcowym rozwiązaniu pojawi się kilka innych przydatnych funkcji, ale zostaną one opisane nieco później.

Implementacja klasy będzie w dużej mierze odzwierciedlała implementację zwykłego słownika. Podczas odwołania do elementu cache możliwe będzie sprawdzenie, czy element został już ustawiony. Jeżeli tak, zostanie zwrócony. Jeżeli nie, wywołana zostanie funkcja pobierająca. Warto zwrócić uwagę, że obiekt reprezentujący cache będzie umożliwiał dostęp do swojej zawartości dla wielu wątków jednocześnie. Wykorzystany zostanie prosty tryb blokowania. Co jakiś czas wewnętrzny mechanizm przejrzy elementy nieużywane i usunie je. Uznałem, że implementacja może ulec zmianie, dlatego postanowiłem przygotować prosty interfejs i się go trzymać:

public interface ICache<in TKey, TItem>
{
    TItem this[TKey key] { get; }

    TItem Add(TKey key, TItem item);

    bool Remove(TKey key);

    void Clear();

    bool ContainsKey(TKey key);

    TItem[] Find(Func<TItem, bool> predicate);

    TItem[] Remove(Func<TItem, bool> predicate);

    void Process(Func<TItem, bool> action);
}

Warto zwrócić uwagę, że interfejs może przyjąć dowolne typy reprezentujące klucz i wartość - zgodnie z założeniami. A co z metodami? Najważniejszą metodą będzie indekser. To on pozwoli nam pobrać wartość zapisaną pod kluczem w sposób słownikowy. Druga metoda, Add pozwoli na ręczne dodanie elementu do słownika. Trzecia, Remove, będzie pozwalała na usunięcie elementu po kluczu. Zdarza się bowiem, że jakiś element uległ zmianie i należy go przedwcześnie unieważnić. Metoda Clear pozwoli na usunięcie całej zawartości. Istnieją takie sytuacje, w których należy unieważnić wszystkie elementy, na przykład podczas zmiany konfiguracji całej aplikacji lub takich zmian, które wymagałyby skomplikowanych algorytmów wyszukiwania i aktualizowania poszczególnych elementów. Kolejna metoda, ContainsKey, to odpowiednik takiej samej metody ze słownika. Wiemy z założeń, że próba pobrania nieistniejącego elementu może wywołać procedurę pozyskania tego elementu. Może to być proces kosztowny. Jeżeli zależy nam tylko na prostej informacji jest/nie ma, tak/nie, wtedy zwykłe sprawdzenie klucza wystarczy.

Pozostałe metody są dodatkiem, ale bardzo przydatnym. Doświadczenie nauczyło mnie, że prędzej czy później jakiś użytkownik zechce przeszukać cache i sprawdzić, czy znajduje się tam jakiś element. Służy do tego funkcja Find, której jedynym argumentem jest predykat. Nie zamierzam wnikać w definicje pojęcia predykat. Ważne jest tylko to, że jest to zwykła funkcja. Wywoływana jest ona dla każdego elementu bufora, a jej zadaniem jest zwrócenie true jeżeli element ma być zwrócony lub false, jeżeli element ma być pominięty. Podobnie działa metoda Remove. Z tą tylko różnicą, że odnalezione elementy zostaną z bufora usunięte. Trzecia z metod przetwarzających cały zbiór, Process, pozwala w dowolny sposób przetworzyć wszystkie elementy bufora. Gdybyśmy na przykład zechcieli zmienić właściwości niektórych elementów, moglibyśmy je przetworzyć właśnie w tej metodzie. Przypomnę, że z założenia takie operacje powinny być synchronizowane i nie dopuszczać jednoczesnego dostępu dla wielu wątków. Metoda przetwarzająca powinna zwrócić true jeżeli chcemy przetwarzać kolejny element lub false, jeżeli chcemy ten proces przedwcześnie zakończyć. Zauważmy, że bufor może być dość duży a same operacje modyfikacji kosztowne. Takie zręczne przetwarzanie pozwala na zmodyfikowanie zawartości bufora fragmentami, co znacznie skraca czas blokady nakładanej na elementy.

Po przeanalizowaniu kilku aplikacji korzystających z buforów podręcznych uznałem, że taki zestaw funkcji jest wystarczający. Czas na implementację.

Implementacja obiektu bufora pamięciowego

Tworząc interfejs miałem na uwadze mnogość potencjalnych implementacji. Najczęściej implementacje takich pamięci podręcznych przechowują swoje dane w pamięci operacyjnej komputera. Spotkałem się jednak z rozwiązaniami, które korzystają z plików. Widziałem także takie, które korzystają z bazy danych. Kosztowne wywołania sieciowe zastępowane są wówczas przez znacznie mniej kosztowne odwołania do lokalnej bazy danych. Każde z tych rozwiązań będzie się mocno różniło od pozostałych. Poznając jednak, mam nadzieję, prosty interfejs, będziemy w stanie zrealizować w podobny sposób te same operacje.

Przyjrzyjmy się najpierw polom i właściwościom klasy - wszak to one decydują o stanie pamięci podręcznej:

public class MemoryCache<TKey, TItem> : ICache<TKey, TItem>
{
    //Domyślny czas, po którym usunięte zostaną nieużywane elementy
    private const int DefaultCollectorInterval = Timeout.Infinite;

    //Zegar okresowo wywołujący mechanizm usuwania przeterminowanych wpisów
    private Timer CollectorTimer { get; set; }
    //Wewnętrzny słownik przechowujący elementy
    private readonly Dictionary<TKey, CacheItem<TItem>> _storage = new Dictionary<TKey, CacheItem<TItem>>();
    //Obiekt synchronizacyjny dla różnych wątków - tylko jeden wątek może w tym samym czasie korzystać z bufora
    private readonly object _synchronizationObject = new object();
    //Funkcja wywoływana w przypadku braku
    private readonly Func<TKey, TItem> _onFetch;

    //Przedłużanie ważności elementów podczas odwołania
    public bool SlidingExpiration { get; set; }

    //Interwał sprawdzania wartości przeterminowanych
    private int _collectorInterval;
    public int CollectorInterval
    {
        get { return _collectorInterval; }
        set
        {
            _collectorInterval = value;
            if (_collectorInterval <= 0)
                _collectorInterval = DefaultCollectorInterval;
            CollectorTimer.Change(_collectorInterval, _collectorInterval);
        }
    }

Pola zostały opatrzone komentarzem, więc nie powinno być z nimi problemu. Warto zwrócić uwagę na pole _onFetch - będzie tu zapamiętana funkcja, która zostanie wywołana w przypadku próby pobrania wartości po nieistniejącym kluczu. Ten klucz zostanie do metody przekazany w postaci argumentu, a zadaniem funkcji będzie zwrócenie gotowego obiektu. Ta funkcja będzie przekazana z zewnątrz - ot takie wstrzyknięcie zależności. Druga, tak podejrzewam, mało przejrzysta rzecz, to właściwość SlidingExpiration. Domyślnie element pamięci cache traci ważność po upłynięciu wskazanego właściwością CollectorInterval czasu. Są jednak przypadki, gdy takie zachowanie nie jest pożądane. Przypuśćmy, że mamy stronę internetową i jakieś informacje powiązane z użytkownikiem odwiedzającym ją. Zwykle taki użytkownik rozpoczyna sesję i trwa ona jakiś czas. Podczas tej sesji wykonuje wiele operacji wymagających danych z pamięci cache. Czy jeżeli cały czas jest aktywny jego rekord powinien zostać zwolniony? Rozwiązaniem jest właśnie przedłużanie ważności. Element pamięci podręcznej zostanie zwolniony dopiero wtedy, gdy system wykryje brak odwołań przez wskazany czasPokazana implementacja gwarantuje istnienie elementów przez wskazany czas. Nie daje jednak gwarancji, że element zostanie usunięty dokładnie po wskazanym czasie. Będzie to w czas w przedziale (CollectorInterval, 2xCollectorInterval). Na koniec wspomnę, że ustawienie zegara na wartości Timeout.Infinite blokuje mechanizm usuwania przeterminowanych wpisów. Pola te będą się pojawiać w prezentowanych w dalszej części metodach.

Tworzenie obiektu klasy MemoryCache

Opisywana klasa może mieć wiele konstruktorów, tak jak każdy inny obiekt. Ja uznałem, że dwa w zupełności wystarczą. Pierwszy z nich będzie wymagał przekazania funkcji pobierającej nowe, nieistniejące elementy. Drugi konstruktor dorzuci wartość wskazującą interwał uruchamiania mechanizmu usuwania elementów przeterminowanych. Jeżeli nie zostanie on przekazany, ustawiany jest na nieskończoność. W praktyce oznacza to zatrzymanie całego mechanizmu. Popatrzmy na listing:

public MemoryCache(Func<TKey, TItem> onFetch) : this(onFetch, DefaultCollectorInterval)
{
}

public MemoryCache(Func<TKey, TItem> onFetch, int collectorIntervalInMs)
{
    CollectorTimer = new Timer(DisposeExpiredEntries, null, DefaultCollectorInterval, DefaultCollectorInterval);
    CollectorInterval = collectorIntervalInMs;
    _onFetch = onFetch;
}

Prostota jest porażająca. Okaże się wkrótce, że inne metody również złożonością nie grzeszą. I tak powinno być.

Dodawanie i pobieranie elementów z cache

Aby coś wyjąć, trzeba najpierw włożyć. Ci, którzy dokładnie przeanalizowali pola z pewnością dostrzegli coś zaskakującego: w słowniku nie są przechowywane elementy typu wskazanego jako typ klasy MemoryCache - TItem. Jest tam typ CacheItem<TItem>>. Jakaż to magia się za tym kryje? Typ ten, oprócz faktycznej wartości, przechowuje znacznik czasu. To na jego podstawie mechanizm sprzątający decyduje o pozostawieniu lub usunięciu wartości. Popatrzmy na definicję klasy pomocniczej:

public class CacheItem<TItem>
{
    public TItem Item { get; set; }
    public DateTime ExpirationTime { get; set; }
}

Znów, nic wielkiego. A jak wygląda dodawanie nowego elementu?

public TItem Add(TKey key, TItem item)
{
    var newCacheItem = new CacheItem<TItem>()
    {
        Item = item,
        ExpirationTime = DateTime.Now.AddMilliseconds(CollectorInterval)
    };
    lock (_synchronizationObject)
    {
        _storage[key] = newCacheItem;
    }
    return newCacheItem.Item;
}

Dwie rzeczy na które warto zwrócić uwagę. Pierwsza - obiekt przerzucany jest do wspomnianej klasy ze znacznikiem, a sam znacznik ustawiany tak, aby stracił ważność po wskazanym czasie. Druga - dostęp do kolekcji jest synchronizowany. Tak będzie w każdej z metod modyfikującej kolekcję. Popatrzmy jeszcze na sposób implementacji indeksera:

public TItem this[TKey key]
{
    get
    {
        lock (_synchronizationObject)
        {
            CacheItem<TItem> cacheItem;
            if (_storage.TryGetValue(key, out cacheItem))
            {
                if (SlidingExpiration && CollectorInterval != Timeout.Infinite)
                {
                    cacheItem.ExpirationTime = DateTime.Now.AddMilliseconds(CollectorInterval);
                }
                return cacheItem.Item;
            }
        }

        TItem newItem = _onFetch(key);
        return Add(key, newItem);
    }
}

Zasada działania jest prosta. Jeżeli wewnętrzny słownik zawiera klucz, zwróć przetrzymywaną tam wartość, jeżeli nie, wywołaj metodę pobierającą i wstaw nowy rekord wykorzystując pokazaną wcześniej metodę Add. Warto zwrócić uwagę, że blokada zakładana jest tylko na czas dostępu do słownika. Pobieranie nowej wartości może być kosztowne, dlatego postanowiłem zezwolić innym wątkom na przejęcie blokady. Bardzo dociekliwi mogą w tym momencie spytać: a co się stanie, jeżeli dwa wątki zażądają tego samego, nieobecnego klucza? No cóż, dwa razy zostanie wywołana metoda pobierająca, a w pamięci cache wyląduje ten, który będzie ostatni. Można to oczywiście naprawić, ale nie uznałem tego za konieczne w tej wersji.

Warto też zwrócić uwagę, że w przypadku ustawienia właściwości SlidingExpiration i interwału mechanizmu sprzątającego różnego od nieskończoności (tylko symbolicznej), przedłużany jest czas ważności pobieranego rekordu. Zobaczmy też, że znacznik czasowy jest całkowicie niewidoczny na zewnątrz klasy. Znacznik wykorzystywany jest tylko wewnętrznie, w szczególności przez tajemniczą do tej pory metodę DisposeExpiredEntries.

Usuwanie niepotrzebnych rekordów

Zdarzenie zegara wywołuje metodę DisposeExpiredEntries:

private void DisposeExpiredEntries(object state)
{
    lock (_synchronizationObject)
    {
        DateTime timeLimit = DateTime.Now;
        var toDelete = _storage.Where(a => a.Value.ExpirationTime < timeLimit).ToArray();
        foreach (var item in toDelete)
        {
            _storage.Remove(item.Key);
        }
    }
}

Metoda, oczywiście rezerwując sobie zasób na wyłączność, wyszukuje wszystkie przeterminowane elementy i usuwa je z wewnętrznego słownika. Operacja przeglądania elementów zależy od wielkości słownika i ma złożoność liniową - jest zatem dość szybka. Usuwanie elementu słownika ma stałą, jednostkową złożoność i również jest dość szybki. Nie należy się wobec tego bać problemów z wydajnością, przynajmniej w tym miejscu.

Czyszczenie zawartości cache i usuwanie elementów

Cache, który nigdy nie usuwa swoich elementów stwarza ryzyko pochłonięcia całych zasobów. W pewnym momencie otrzymamy nieprzyzwoity wyjątek OutOfMemoryException lub system operacyjny zacznie stosować plik wymiany co w konsekwencji doprowadzi do drastycznego spadku wydajności wszystkich aplikacji. Tego nie chcemy. To dlatego elementy są sprawdzane i usuwane po wskazanym czasie i to dlatego istnieją pokazane poniżej metody:

public bool Remove(TKey key)
{
    lock (_synchronizationObject)
    {
        return _storage.Remove(key);
    }
}

public void Clear()
{
    lock (_synchronizationObject)
    {
        _storage.Clear();
    }
}

Pierwsza z nich usuwa element ze słownika, druga usuwa wszystkie elementy słownika. Czasami istnieje potrzeba usunięcia elementów spełniających podane kryterium. Na przykład usunięcie wszystkich wpisów o wyliczonych uprawnieniach użytkowników, jeżeli zmieniły się uprawnienia jednej z ról, do której są przypisani. Nie da się tego zrobić po kluczu. Nierozsądne może się wydać usuwanie wszystkich wpisów. To dlatego powstała kolejna metoda:

public TItem[] Remove(Func<TItem, bool> predicate)
{
    lock (_synchronizationObject)
    {
        var toDelete = _storage.Where(a => predicate(a.Value.Item)).ToArray();
        foreach (var item in toDelete)
        {
            _storage.Remove(item.Key);
        }
        return toDelete.Select(a => a.Value.Item).ToArray();
    }
}

Parametrem metody jest dowolna funckja, która na podstawie przekazanej wartości ma stwierdzić, czy element należy usunąć (zwraca true), czy też zostawić (zwraca false). Metoda zwraca tablicę obiektów, które zostały usunięte.

Inne metody pomocnicze

Interfejs ICache ma jeszcze trzy metody, które nie zostały opisane. Metoda ContainsKey nie jest szczególnie interesująca, bo jedyne co robi to wywołuje taką samą metodę z wewnętrznego słownika:

public bool ContainsKey(TKey key)
{
    lock (_synchronizationObject)
    {
        return _storage.ContainsKey(key);
    }
}

Metoda Find służy do odnalezienia wszystkich elementów cache spełniających podane kryteria. Działa bardzo podobnie do metody Remove z predykatem:

public TItem[] Find(Func<TItem, bool> predicate)
{
    lock (_synchronizationObject)
    {
        var filteredItems = from item in _storage.Values
            where predicate(item.Item)
            select item.Item;
        return filteredItems.ToArray();
    }
}

Jeszcze raz zwrócę uwagę na synchronizację wszystkich funkcji i zabezpieczenie przed modyfikacją słownika przez inne wątki podczas wyszukiwania.

Została w zasadzie jeszcze jedna metoda. Tak trochę furtka dla zastosowań specjalnych. Pozwala ona wywołać przekazaną w postaci parametru metodę dla każdego z elementów słownika. Służy głównie do modyfikacji istniejących w pamięci cache obiektów.

public void Process(Func<TItem, bool> action)
{
    lock (_synchronizationObject)
    {
        foreach (var item in _storage.Values)
        {
            if (!action(item.Item))
                return;
        }
    }
}

Jeżeli wiemy, że obiekty te uległy zmianie, możemy je unieważnić (usunąć). Próba odwołania się do nich spowoduje pobranie świeżej wersji przy pomocy metody _onFetch. Ale, możemy też je zmodyfikować metodą Process i uniknąć kosztownych wywołań.

Wykorzystanie klasy MemoryCache

Starałem się, aby korzystanie z klasy było jak najłatwiejsze. Popatrzmy jak to wygląda w praktyce:

private static void Main()
{
    ICache<string, string> cache = new MemoryCache<string, string>(FetchRecord, 1000)
    {
        SlidingExpiration = false
    };

    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine("A ({0} ms) = {1}", i * 300, cache["A"]);
        Thread.Sleep(300);
    }
    Console.WriteLine("B = {0}", cache["B"]);

    Console.WriteLine("cache=[{0}]", string.Join(", ", cache.Find(a=>true)));
    cache.Clear();
    Console.WriteLine("cache=[{0}]", string.Join(", ", cache.Find(a => true)));
    Console.ReadLine();
}

private static string FetchRecord(string key)
{
    Console.WriteLine("Fetch record {0}", key);
    return DateTime.Now.ToLongTimeString();
}

Po uruchomieniu programu i odczekaniu kilku sekund otrzymamy rezultat podobny do poniższego:

Fetch record A
A (0 ms) = 17:45:55
A (300 ms) = 17:45:55
A (600 ms) = 17:45:55
A (900 ms) = 17:45:55
A (1200 ms) = 17:45:55
A (1500 ms) = 17:45:55
A (1800 ms) = 17:45:55
Fetch record A
A (2100 ms) = 17:45:58
A (2400 ms) = 17:45:58
A (2700 ms) = 17:45:58
Fetch record B
B = 17:45:58
cache=[17:45:58, 17:45:58]
cache=[]

Jak to zadziałało? Podczas pierwszego odwołania okazało się, że rekordu nie ma i należy go pobrać. Stąd w pierwsze kolejności komunikat Fetch record A, a dopiero po nim dane. Kolejne odwołania zwracają wynik już bezpośrednio z pamięci. Po około sekundzie (1000 milisekund) algorytm sprawdza czy są jakieś elementy do usunięcia. W tym przypadku okazało się, że jeszcze nie. Rekord pobrany sekundę temu nie jest starszy niż sekundaAlgorytm nie jest deterministyczny. Wszystko zależy od tego, w jaki sposób system operacyjny będzie nadawał priorytety zadaniom. więc nic się nie dzieje. Program pobiera wartości z pamięci cache. Po dwóch sekundach znów uruchamia się mechanizm. Tym razem okazuje się, że rekord z kluczem A jest przeterminowany. Zostaje on usunięty. To dlatego po 2,1 sekundy znów musi nastąpić odwołanie do zewnętrznego źródła (w tym przypadku zwykłej metody). Oczywiście odwołanie się do innego klucza również spowoduje wywołanie metody pobierającej. Na końcu pokazane jest przykładowe wywołanie metody Find z dość śmiesznym predykatem: wartość zawsze będzie równa true, dlatego wszystkie rekordy zostaną zwrócone. W ten sposób możemy wyświetlić sobie zawartość całej pamięci podręcznej.

Podsumowanie

Pokazany przykład nie jest szczególnie rozbudowany. Można sobie łatwo wyobrazić kilka rzeczy, które z pewnością pozwoliłyby rozszerzyć możliwości klasy. Po pierwsze, przybliżyć czas usuwania elementów przeterminowanych. W podanym przykładzie element może istnieć aż dwa razy dłużej niż powinno to wynikać z wartości przekazanego parametru. Można to zrobić wprowadzając inną wartość do mechanizmu zegara, a inną do ważności. Można oczywiście te dwie wartości od siebie uzależnić (np. wywoływać metodę sprzątającą pięć razy częściej niż wynosi okres ważności). Można sobie wyobrazić również pamięć podręczną na różne obiekty. Da się wprawdzie jako typ wartości ustawić object, ale będziemy wtedy zmuszeni do jawnych rzutowań. To z kolei sugeruje dodanie odpowiedniej metody wykonującej rzutowanie o kontrolę typów. Można, a jakże by inaczej, zezwolić na kontrolowanie czasu życia poszczególnych obiektów przekazując tę wartość w parametrze podczas ręcznego dodawania. Wszystko to są jakieś pomysły, które mogłyby się przydać. Gdyby pojawiło się zapotrzebowanie, o czym dowiem się z komentarzy, postaram się temat rozwinąć.

Kategoria:C#

, 2015-09-18

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
chcę dodać kolumnę, która będzie połączeniem dwóch innych istniejących już kolumn, jak powinien wyglądać scrypt?
Przydałyby się jeszcze 2 rzeczy do cz. 3 i byłoby superanckie.
1. Na starcie sortuje wg jakiejś kolumny i tam jest już strzałeczka. Widok takiej strzałeczki daje znać użytkownikowi, że taką tabele można sortować, a na razie pojawia się ona tylko po kliknięciu.
2. Uwzględnienie polskich znaków, bo np. przy sortowaniu Nazwisk i Imion jest to bardzo uciążliwe.
Ogólnie bardzo fajnie i prosto.
PS. Jest ten artykuł z jQuery już dostępny.
bardo ciekawe , można dzięki takim wpisom dostrzec wędkę..
Bardzo dziękuję za jasne tłumaczenie z dobrze dobranym przykładem!