Zamiast tak odpisywać w komentarzach postanowiłem dodać swój wpis i kawałek kodu.
Podczas pisania założyłem że:
- index nie może być mniejszy niż 0
- pierwszy element wskazuje ma index 0
- skoro ma być to tablica rzadka wiec iterowanie powinno również zwracać domyślne wartości dla niezdefiniowanych elementów
- iterowanie jest wykonywane do elementu o najwyższym indeksie
- można zdefiniować jaka jest domyślna wartość, jeżeli nie brana jest domyślna dla typu elementów kolekcji
Zastosowanie IEnumerable<T> ma na celu ujednolicenie sposobu poruszania się po kolekcji (element po elemencie, od początku do końca) co wykorzystano w klasach LINQ.
public class GenericArray<T>
: IEnumerable<T> // co zonacza że również IEnumerable dla kompatybilności z .NET 1.1
{
public GenericArray()
: this(default(T)) // domyślna wartość dla typu (np. int -> 0, dla klass null)
{
}
// Pozwala określić domyślną wartość dla elemntów pustych
public GenericArray(T defaultValue)
{
_defaultValue = defaultValue;
}
private readonly T _defaultValue;
private readonly IDictionary<int, T> _items = new Dictionary<int, T>();
private int _itemsCount; // ile mamy elementów (nie jest to ilość elementów w _items)
public T this[int index]
{
get
{
return _items.ContainsKey(index)
? _items[index] // jeżeli nasza wewnętrza tabblica przechwuje wartość dal wskazanj pozycji
: _defaultValue; // jezeli jej nie ma
}
set
{
if (index <= 0)
{
throw new IndexOutOfRangeException();
}
// Sprawdzamy czy wartość jest różna od domyślnej.
if (!EqualityComparer<T>.Default.Equals(_defaultValue, value))
{
_items[index] = value;
return;
}
// Usuwamy jeśli jest
if (_items.ContainsKey(index))
{
_items.Remove(index);
}
}
}
public IEnumerator<T> GetEnumerator() // uzycie yield return "automatycznie tworzy" enumerator
{
var index = 0;
foreach (var item in _items.OrderBy(k => k.Key)) // Wykorzytsanie LINQ do sortowania IDictionarty po kluczu
{
while (index < item.Key)
{
index++;
yield return _defaultValue;
}
index++;
yield return item.Value;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator(); // IEnumerator<T> tez implemntuje IEnumerator :)
}
}
Wykorzystałem tu kilka elementów .NET
-
!EqualityComparer<T>.Default
w ten sposób nie interesuje mnie jak porównywane są elementy czy typ T.
-
OrderBy
to metoda LINQ do sortowania. W końcu Dictionary to tez kolekcja a każda kolekcja jest IEnumerable<jakiś typ>, to jakiś typ to KeyValuePair<TKey, TValue> ... jeszcze więcej generyków
-
yield return
ułatwia zwracanie elementów kolekcji (gdy metoda ma zwracać IEnumerable<jakis typ="typ">). Kompilator generuje dla nas kod enumeratora
Poniżej chciałem pokazać kilka przykładów użycia LINQ (.NET 4.0). Żeby nie było ściemy do operacji na kolekcji używam osobnej zmiennej tylko typu IEnumerable<int>:
var instance = new GenericArray<int>(); // domyślna wartośc pusta
var instance = new GenericArray<int>(); // domyślna kolekcja pusta
var instanceEnumerable = (IEnumerable<int>)instance;
// brak elemntów
AssertThat(instanceEnumerable.Count() == 0);
AssertThat(!instanceEnumerable.Any()); // to samo co wyżej ale bez zliczania wszytkich elementów
instance[9] = 10;
AssertThat(instanceEnumerable.Count() == 10);
AssertThat(instanceEnumerable.Any());
AssertThat(instance[9] == 10);
AssertThat(instance[4] == 0);
AssertThat(instanceEnumerable.ElementAt(9) == 10);
AssertThat(instanceEnumerable.Skip(9).First() == 10);
AssertThat(instanceEnumerable.ElementAt(4) == 0);
AssertThat(instanceEnumerable.Skip(4).First() == 0);
AssertThat(instanceEnumerable.Contains(10)); // Dzieki LINQ
AssertThat(instanceEnumerable.Last() == 10);
instance[4] = 5;
AssertThat(instanceEnumerable.Count() == 10);
var list = instance.ToList(); // Zapisanie danych w nowej tablicy (wszystkich, nawet pustych)
AssertThat(list.Count == 10);
AssertThat(list[9] == 10);
AssertThat(list[4] == 5);
instance[9] = 0;
AssertThat(instanceEnumerable.Count() == 5);
AssertThat(instanceEnumerable.Any());
AssertThat(instance[4] == 5);
instance[4] = 0;
AssertThat(instanceEnumerable.Count() == 0);
AssertThat(!instanceEnumerable.Any());
gdzie meteoda AsssertThat to taki prosty silnik testujący aby nie wprowadzać żadnego farmeworka i nie gmatwać przykładu:
public void AssertThat(bool isValid)
{
if (!isValid)
{
const string message = "Assertion failed.";
Console.WriteLine(message);
throw new Exception(message);
}
}
Implementacja ma pewne luki, ale one wynikają z założeń. Np. taka operacja przejdzie nawet na nowo stworzonej instancji.
AssertThat(instance[999] == 0);
ale
instance.Count() = 0);
No tak Count() (metoda LINQ nie ta z ICollection) zwraca nam 0 a tu mamy 1000 element. Tak działa przedstawiona implementacja. Nie ma definicji rozmiaru tablicy a Count() przechodzi po wszystkich elementach i je zlicza.
No własnie... IEnumerable<T> pozwala nam przechodzić po kolekcji elementów niezależnie od implementacji... ale tylko tyle nie pozwala nam zarządzać ta kolekcją. Do tego jest służy interfejs ICollection<T> pozwalając nam Dodawać, Usuwać, podmieniać czy dowiedzieć się ile mamy elementów. O ile chcielibyśmy aby nasza klasa mogła zastąpić na przykład klasę List<T> wtedy powinniśmy się nad zastanowić nad jej implementacją. Wspominając o tym interfejsie bardziej chciałem wskazać dalszy kierunek rozwoju implementacji a przy okazji rozważań o zachowaniu implementacji (np to zliczanie). "Dobra praktyka" mówi tylko o tym że każdy typ reprezentujący kolekcję powinien implementować IEnumerable<T>.
Mistzzz'u, dobrze ze piszesz kod pokazujący testowanie, to też "dobra praktyka". Może zainteresuje Cię nUnit lub xUnit albo MSTest. To jak je napisałem to takie małe demo aby je nieco zautomatyzować automatyzować ;).