IndekseryCzęść NR.2

W poprzednim wpisie pokazałem jak operować bitami za pomocą odpowiednich operatorów.

Zatrzymajmy się na chwilę i przypominajmy sobie na czym polega problem.

Chcemy użyć typu int jako tablicy, która przechowuje bity. W podobnym stylu typ string to tablica znaków. Przykładowo, gdybym chciał uzyskać dostęp do 6 miejsca bitowego zrobiłbym to tak.

int liczbaZBitów = 32;
liczbaZBitów[6] = true;

Niestety typ int nie może być traktowany notacją nawiasów kwadratowych. Ta notacja działa tylko dla typów, które zachowują się tablice i mają swoje własne indeksery. 

int[]
Rozwiązanie tego problemu polega na stworzeniu nowego typu, który zachowuje się jak tablica wartości bool ,ale jest implementowana jak typ int.


Nasz typ będzie korzystał z indeksera. Nazwijmy ten nowy typ “BitsInt .

BitsInt będzie składać się z konstruktora, który będzie zawierał wartość int,ale pomysł polega na tym aby użyć BitsInt jako tablicy wartości logicznych.

Typ ten tworzy tylko nową funkcjonalność dla typu int dlatego nie ma sensu tworzenia klasy dla tego rozwiązania więc skorzystamy ze struktury. Z samym konstruktorem BitsInt wygląda tak, brakuje mu indeksera.

struct BitsInt 
{
    public BitsInt(int intiBitValue)
    {
        bits = intiBitValue;
    }

    private int bits;
}

Aby zdefiniować indekser trzeba użyć notacji, która jest kombinacją pomiędzy właściwościami  a tablicą.

Do przedstawienia indeksera używamy słowa kluczowego this .Potem precyzujesz  typ wartości, która jest zwracana przez indekser. Później musisz określić typ wartości, który jest użyty poprzez indeks pomiędzy nawiasami kwadratowymi.

Indekser w tej strukturze używa int-a jako typu indeksu i zwraca on wartość bool.

struct BitsInt 
{
    public bool this [int index]
    {
        get {
            return (bits & (1 << index)) != 0;
        }
        set {
            if (value)
                bits |= (1 << index);
            else bits &= ~(1 << index);
        }
    }

    public BitsInt(int intiBitValue)
    {
        bits = intiBitValue;
    }

    private int bits;
}

Na co trzeba zwrócić uwagę:

  • Indeksery nie są metodami : Nie ma tutaj nawiasów okrągłych zawierających parametry ,ale są nawiasy kwadratowe, które określają indeks. Ten indeks jest używany do określenia elementu, do którego chcemy mieć dostęp.
  • Wszystkie indeksery używają słowa kluczowego this. Klasa i struktura mogą zdefiniować tylko jeden indekser i jest on zawsze nazwany "this".
  • Indeksery posiadają modyfikatory getset tak jak właściwości. W tym przykładzie posiadają one wyrażenia, które omówiłem wcześniej tutaj.
  • Indeks jest określany przy deklaracji indeksera.

Swoją drogą mój kod nie sprawdza czy indeks wychodzi poza zakres. Ale kod nie zwróci wyjątku, jeśli tak się stanie.

Po deklaracji indeksera możesz użyć zmiennej typu BitsInt zamiast int i użyć notacji nawiasów kwadratowych.

BitsInt bity = new BitsInt(30); //30 ma reprezentacje 00011110 
bool wy5 = bity[5]; //false; 
bool wy4 = bity[4]; // true 
bool wy3 = bity[3]; // true 
bool wy2 = bity[2]; // true 
bool wy1 = bity[1]; // true
bool wy0 = bity[0]; //false 
bity[0] = true; //ustawienie indeksu 0 na wartość 1 true 
bity[1] = false; //ustawienie indeksu 1 na wartość false 0 
//wartość liczbowa teraz wynosi 29 

Teraz kod jest bardziej czytelny. Mam bezpośredni dostęp do pojedynczych bitów w typie int mogę je odczytać i je zmieniać.

Zrozumienie działania

Kiedy odczytujesz indekser kompilator automatycznie tłumaczyć twój kod w stylu tablicy na wywołanie bloku get w indekserze. Jak to widać na przykładzie:

bool wy0 = bity[0]; //false

To wyrażenie konwertuje się na wywołanie bloku GET dla zmiennej “bity” i indeks argumentu jest ustawiany na 0.

W podobny sposób, jeśli zapisujesz wartość do indeksera kompilator automatycznie tłumaczy kod w stylu tablic na wywołanie bloku SET.

bity[1] = false; //ustawienie indeksu 1 na wartość false 0

To wyrażenie jest konwertowane na wywołanie bloku SET dla zmiennej "bity” i gdzie indeks jest ustawiony na 4. Tak jak ze zwykłymi właściwościami dane, które mają nadpisać indekser (w typ wypadku jest to wartość false) jest dostępna wewnątrz w bloku SET pod słowem kluczowym value.

Typ value jest dokładnie taki sam jak typ indeksera, czyli w tym wypadku bool.

Jest możliwe użycie indeksera w kontekście połączonych operacji odczytu i zapis. W tym wypadku blok GET i SET są używane razem. Spójrz na poniższe wyrażenie, które przy pomocy operatora XOR ( ^ ) odwraca wartość bitu numer 5.

bity[5] ^= true;
bity[5] = bity[5] ^ true; //te same wyrażenie

Ten kod wywołuje równocześnie blok GET i SET.

Podobnie we właściwościach możesz nie napisać jednego z tych bloków, czyniąc indekser tylko do odczytu albo do zapisu.

Porównywanie indekserów i tablic

Kiedy używasz indekserów piszesz wyrażenia, które przypominają tablice. Jednakże są pewne poważne różnice pomiędzy indekserami  a tablicami.

  • Indeksery mogą używać nienumerycznych indeksów. Mogą być przykładowo napisem string. Oczywiście wtedy spełniają zupełnie inne cele niż dostęp do bitów w typie   int. Jak sobie wyobrażasz indeksowanie po napisach w takim przypadku. Jest to jednak możliwe
    
    public int this[string name] { set { } }

Wiele kolekcji tak jak HashTable implementuje asocjacyjny wygląd do par kluczy i wartości, które implementują indeksery. Powstał alternatywny sposób użycia metody Add, która dodaje nowe wartości do kolekcji. Poprzez iteracje przez właściwość Values w celu zlokalizowania twojej wartość w kodzie. Jeśli jej nie ma tworzy ją.

System.Collections.Hashtable rokGier = new Hashtable();

rokGier.Add("Mortal Kombat", 1992);

//dzięki indekserom można to zrobić w alternatywny sposób 
rokGier["Mortal Kombat"] = 1992;
  • Indeksery mogą być przeciążane (tak jak metody), gdy tablice oczywiście tego nie potrafią.
    class PhoneBook 
    {
        public Name this[PhoneNumber number] { set { } }
        public PhoneNumber this[Name name] { set { } }       
    }

indeksery

  • Indeksery nie mogą używać słów kluczowych refout zupełnie tak jak właściwości.

Interfejs i indeksery

Możesz zadeklarować indekser w interfejsie. Aby to zrobić podobnie jak z właściwościami ciała bloków SET i GET muszą być zastąpione myślnikiem. Jakakolwiek klasa czy struktura, która będzie implementować interfejs musi implementować te bloki indeksora, które są zadeklarowane w interfejsie.

interface IInsideInt 
{
    bool this[int index] { get; set;}
}

struct InsideInt : IInsideInt 
{
    public bool this[int index]
    {
        get {
            throw new NotImplementedException();
        }
        set {
            throw new NotImplementedException();
        }
    }
}

Klasa implementująca indekser może deklarować indekser jako wirtualny. To pozwala następnym klasom pochodnym na nadpisanie bloków GET i SET.

struct InsideInt : IInsideInt 
{
    public virtual bool this[int index]
    {
        get {
            throw new NotImplementedException();
        }
        set {
            throw new NotImplementedException();
        }
    }
}

Analogicznie w klasie abstrakcyjnej możesz zadeklarować abstrakcyjny indekser. Trzeba tylko pamiętać o tym ,że struktura nie może dziedziczyć po klasie abstrakcyjnej ,ale po interfejsie już tak.

abstract class InsideInt 
{
    public abstract bool this[int index] { get; set;}
}

Wracając do interfejsów może być on też zaimplementowany w stylu explicit. Indekser wtedy nie może być publiczny i niewirtualny .

struct InsideInt : IInsideInt 
{
    bool IInsideInt.this[int index]
    {
        get {
            throw new NotImplementedException();
        }
        set {
            throw new NotImplementedException();
        }
    }
}

Co dalej:

Tyle na razie w tym wpisie następnym razem pokażę, dlaczego indeksery są bardziej użyteczne niż tablice użyte we właściwościach.