Dlaczego słownik jest lepszy niż Hashtable w C #?


1395

W większości języków programowania preferowane są słowniki zamiast tablic skrótów. Jakie są tego przyczyny?


21
> To niekoniecznie jest prawdą. Tabela skrótów jest implementacją słownika. W tym typowy i może być domyślny w .NET, ale z definicji nie jest jedyny. Nie jestem pewien, czy jest to wymagane przez standard ECMA, ale dokumentacja MSDN bardzo wyraźnie nazywa to zaimplementowaniem jako hashtable. Zapewniają nawet klasę SortedList na wypadek, gdy alternatywa jest bardziej rozsądna.
Promit

15
@ Promit Zawsze myślałem, że Dictionaryto implementacja Hashtable.
b1nary.atr0phy

2
Myślę, że powodem jest to, że w słowniku możesz zdefiniować typ klucza i wartość dla siebie. Hashtable może tylko pobierać obiekty i zapisywać pary w oparciu o skrót (z object.GetHashCode ()).
Radinator,

2
@ Dan Twoje roszczenie jest dość błędne ... tabela skrótów zawiera tylko jedno wystąpienie każdego klucza, a wyszukiwanie nigdy nie daje wielu wpisów; jeśli chcesz skojarzyć wiele wartości z każdym kluczem, ustaw wartość tablicy skrótów na listę wartości. Nie ma takiej struktury danych jak „Słownik” ... Słownik to po prostu nazwa, której niektóre biblioteki używają do tabeli skrótów. np HashTable. nazywana jest nieogólna tablica skrótu C # . Kiedy dodali ogólne do języka, nazwali wersję ogólną Dictionary. Oba są tabelami skrótów.
Jim Balter

3
@Dan Twoje roszczenie jest błędne ... tablica skrótów ( en.wikipedia.org/wiki/Hash_table ) to szczególna implementacja słownika, inaczej tablica asocjacyjna ( en.wikipedia.org/wiki/Associative_array ), i będąca słownik, zawiera tylko jedną instancję każdego klucza, a wyszukiwanie nigdy nie daje wielu pozycji; jeśli chcesz skojarzyć wiele wartości z każdym kluczem, ustaw wartość tablicy skrótów na listę wartości. A klasy .NET Dictionary i Hashtable to tabele skrótów.
Jim Balter

Odpowiedzi:


1568

Pod względem wartości Słownik jest (koncepcyjnie) tabelą skrótów.

Jeśli miałeś na myśli „dlaczego używamy Dictionary<TKey, TValue>klasy zamiast Hashtableklasy?”, To jest łatwa odpowiedź: Dictionary<TKey, TValue>jest to rodzaj ogólny, Hashtablenie jest. Oznacza to, że zyskujesz bezpieczeństwo typu Dictionary<TKey, TValue>, ponieważ nie możesz wstawić do niego żadnego losowego obiektu i nie musisz rzutować wartości, które bierzesz.

Co ciekawe, Dictionary<TKey, TValue>implementacja w .NET Framework oparta jest na Hashtable, jak można powiedzieć z tego komentarza w jego kodzie źródłowym:

Słownik ogólny został skopiowany ze źródła Hashtable

Źródło


393
A także kolekcje ogólne są znacznie szybsze, ponieważ nie ma boksu / rozpakowywania
Chris S

6
Nie jestem pewien co do Hashtable z powyższym stwierdzeniem, ale dla ArrayList vs. List <t> to prawda
Chris S

36
Hashtable używa Object do przechowywania rzeczy wewnętrznie (tylko nie-ogólny sposób, aby to zrobić), więc musiałby również spakować / rozpakować.
Guvante

16
@BrianJ: „Tabela skrótów” (dwa słowa) to termin informatyki dla tego rodzaju struktury; Słownik jest specyficzną implementacją. Tabela HashTable z grubsza odpowiada słownikowi <obiekt, obiekt> (choć z nieco innymi interfejsami), ale oba są implementacjami koncepcji tablicy skrótów. I oczywiście, aby jeszcze bardziej pomylić sprawy, niektóre języki nazywają swoje tabele skrótów „słownikami” (np. Python) - ale właściwym terminem CS jest wciąż tabela skrótów.
Michael Madsen

32
@BrianJ: Zarówno HashTable(klasa), jak i Dictionary(klasa) są tablicami skrótu (koncepcja), ale a HashTablenie jest Dictionaryani nie jest Dictionarya HashTable. Są one używane w bardzo podobnych stylach i Dictionary<Object,Object>mogą działać w taki sam nietypowy sposób jak a HashTable, ale nie dzielą bezpośrednio żadnego kodu (chociaż części mogą być implementowane w bardzo podobny sposób).
Michael Madsen

625

Dictionary<<< >>> Hashtableróżnice:

  • Ogólne <<< >>> Nie ogólne
  • Potrzebuje własnej synchronizacji wątków <<< >>> Oferuje bezpieczną wersję wątkuSynchronized() metodą
  • Element wymieniony: KeyValuePair<<< >>> Element wymieniony:DictionaryEntry
  • Nowsze (> .NET 2.0 ) <<< >>> Starsze (od .NET 1.0 )
  • jest w System.Collections.Generic <<< >>> znajduje się w System.Collections
  • Żądanie na nieistniejący klucz zgłasza wyjątek <<< >>> Żądanie na nieistniejący klucz zwraca null
  • potencjalnie nieco szybciej dla typów wartości <<< >>> nieco wolniej (wymaga boksu / rozpakowania) dla typów wartości

Dictionary/ Hashtablepodobieństwa:

  • Oba są wewnętrznie hashtables == szybki dostęp do danych wielu pozycji według klucza
  • Oba potrzebują niezmiennych i unikalnych kluczy
  • Klucze obu wymagają własnej GetHashCode()metody

Podobne kolekcje .NET (kandydaci do użycia zamiast Dictionary i Hashtable):

  • ConcurrentDictionary- wątek bezpieczny (można bezpiecznie uzyskać dostęp z kilku wątków jednocześnie)
  • HybridDictionary- zoptymalizowana wydajność (dla kilku przedmiotów, a także dla wielu przedmiotów)
  • OrderedDictionary- dostęp do wartości można uzyskać za pomocą indeksu int (według kolejności, w której elementy zostały dodane)
  • SortedDictionary- pozycje sortowane automatycznie
  • StringDictionary- silnie napisane i zoptymalizowane pod kątem łańcuchów

11
@ Guillaume86, dlatego zamiast tego używasz TryGetValue msdn.microsoft.com/en-us/library/bb347013.aspx
Trident D'Gao

2
+1 dla StringDictionary... btw StringDictionaryto nie to samo, co Dictionary<string, string>przy użyciu domyślnego konstruktora.
Cheng Chen,

ParallelExtensionsExtras @ code.msdn.microsoft.com/windowsdesktop/... zawiera ObservableConcurrentDictionary, który jest świetnym wiązaniem jodłowym, a także współbieżnością.
VoteCoffee

3
niesamowite wyjaśnienie, to naprawdę miłe, że wymieniłeś również podobieństwa, aby zmniejszyć pytania, które mogą
przyjść ci


178

Ponieważ Dictionaryjest to klasa ogólna ( Dictionary<TKey, TValue>), więc dostęp do jej zawartości jest bezpieczny dla typu (tzn. Nie trzeba rzutować z Object, tak jak w przypadku a Hashtable).

Porównać

var customers = new Dictionary<string, Customer>();
...
Customer customer = customers["Ali G"];

do

var customers = new Hashtable();
...
Customer customer = customers["Ali G"] as Customer;

Jednak Dictionaryjest wewnętrznie implementowany jako tablica skrótów, więc technicznie działa tak samo.


88

FYI: W .NET, Hashtablewątek jest bezpieczny do użytku przez wiele wątków czytnika i jeden wątek do pisania, podczas gdy w Dictionarypublicznych elementach statycznych jest wątek bezpieczny, ale nie można zagwarantować, że jakikolwiek element instancji jest bezpieczny.

Z tego powodu musieliśmy zmienić wszystkie nasze słowniki Hashtable.


10
Zabawa. Kod źródłowy Dictionary <T> wygląda na znacznie czystszy i szybszy. Lepszym rozwiązaniem może być użycie słownika i wdrożenie własnej synchronizacji. Jeśli odczyty ze Słownika absolutnie muszą być aktualne, wystarczy zsynchronizować dostęp do metod odczytu / zapisu Słownika. Byłoby dużo blokowania, ale byłoby poprawne.
Triynko

10
Alternatywnie, jeśli twoje odczyty nie muszą być absolutnie aktualne, możesz traktować słownik jako niezmienny. Następnie możesz pobrać odniesienie do słownika i zwiększyć wydajność, nie synchronizując odczytów w ogóle (ponieważ jest niezmienny i z natury bezpieczny dla wątków). Aby go zaktualizować, konstruujesz w tle kompletną zaktualizowaną kopię Słownika, a następnie po prostu zamieniasz odwołanie za pomocą Interlocked.CompareExchange (zakładając, że jeden wątek do pisania; wiele wątków do pisania wymagałoby synchronizacji aktualizacji).
Triynko

38
.Net 4.0 dodał ConcurrentDictionaryklasę, w której zaimplementowano wszystkie metody publiczne / chronione, aby zapewnić bezpieczeństwo wątków. Jeśli nie potrzebujesz obsługi starszych platform, pozwoli to zastąpić Hashtablewielowątkowy kod: msdn.microsoft.com/en-us/library/dd287191.aspx
Dan Is Fiddling By Firelight

anonimowy na ratunek. Fajna odpowiedź.
unkulunkulu,

5
Pamiętam, że HashTable jest bezpieczny dla wątków tylko w czytniku-pisarzu w scenariuszu, w którym informacje nigdy nie są usuwane z tabeli. Jeśli czytelnik prosi o element znajdujący się w tabeli podczas usuwania innego elementu, a czytelnik szukałby tego elementu w więcej niż jednym miejscu, możliwe jest, że podczas przeszukiwania czytnik pisarz może przenieść element z miejsca, które nie zostało zbadane, do tego, które ma, co powoduje fałszywe zgłoszenie, że przedmiot nie istnieje.
supercat

68

W .NET różnica między Dictionary<,>i HashTablepolega przede wszystkim na tym, że ten pierwszy jest typem ogólnym, więc zyskujesz wszystkie zalety generyczne w zakresie statycznego sprawdzania typu (i zmniejszonego boksu, ale nie jest to tak duże, jak ludzie myślą w warunki wydajności - jednak boks ma określony koszt pamięci).


34

Ludzie mówią, że słownik jest taki sam jak tabela skrótów.

To niekoniecznie jest prawdą. Tabela skrótów jest jednym ze sposobów implementacji słownika. W tym typowy i może być domyślny w .NET w Dictionaryklasie, ale z definicji nie jest jedyny.

Równie dobrze można zaimplementować słownik za pomocą połączonej listy lub drzewa wyszukiwania, po prostu nie byłby on tak wydajny (dla niektórych wskaźników wydajności).


4
Dokumenty MS mówią: „Pobieranie wartości za pomocą jej klucza jest bardzo szybkie, zbliżone do O (1), ponieważ klasa Dictionary <(Of <(TKey, TValue>)>) jest zaimplementowana jako tablica skrótów.” - więc powinieneś mieć gwarancję, że masz do czynienia z tablicą hashtable Dictionary<K,V>. IDictionary<K,V>może być cokolwiek :)
snemarch

13
@ rix0rrr - Myślę, że masz to odwrotnie, słownik używa HashTable, a HashTable nie używa Dictionary.
Joseph Hamilton,

8
@JosephHamilton - rix0rrr ma rację: „Tabela skrótów jest implementacją słownika ”. Ma na myśli pojęcie „słownik”, a nie klasę (zwróć uwagę na małe litery). Pod względem koncepcyjnym tablica skrótów implementuje interfejs słownika. W .NET Dictionary używa tabeli skrótów do implementacji IDictionary. Jest bałagan;)
Robert Hensing

Mówiłem o .NET, ponieważ o tym wspomniał w swojej odpowiedzi.
Joseph Hamilton

2
@JosephHamilton: implementuje (lub implementuje ) nawet nie oznacza tego samego, co używa . Wręcz przeciwnie. Być może byłoby to jaśniejsze, gdyby powiedział to nieco inaczej (ale o tym samym znaczeniu): „tablica skrótów jest jednym ze sposobów implementacji słownika”. Oznacza to, że jeśli chcesz funkcjonalność słownika, jednym ze sposobów (w celu zaimplementowania słownika) jest użycie tablicy hashtable.
ToolmakerSteve

21

Collectionsi Genericssą przydatne do obsługi grupy obiektów. W .NET wszystkie obiekty kolekcji znajdują się w interfejsie IEnumerable, który z kolei ma ArrayList(Index-Value))& HashTable(Key-Value). Po .NET Framework 2.0, ArrayListi HashTablezostały zastąpione przez List& Dictionary. Teraz Arraylist& HashTablenie są już używane w dzisiejszych projektach.

Różnica między HashTable& Dictionary, Dictionaryjest ogólna, gdy Hastablenie jest ogólna. Możemy dodać dowolny typ obiektu HashTable, ale podczas pobierania musimy rzucić go na wymagany typ. Więc nie jest to bezpieczne. Ale dictionary, deklarując się, możemy określić typ klucza i wartości, więc nie ma potrzeby rzucania podczas pobierania.

Spójrzmy na przykład:

HashTable

class HashTableProgram
{
    static void Main(string[] args)
    {
        Hashtable ht = new Hashtable();
        ht.Add(1, "One");
        ht.Add(2, "Two");
        ht.Add(3, "Three");
        foreach (DictionaryEntry de in ht)
        {
            int Key = (int)de.Key; //Casting
            string value = de.Value.ToString(); //Casting
            Console.WriteLine(Key + " " + value);
        }

    }
}

Słownik,

class DictionaryProgram
{
    static void Main(string[] args)
    {
        Dictionary<int, string> dt = new Dictionary<int, string>();
        dt.Add(1, "One");
        dt.Add(2, "Two");
        dt.Add(3, "Three");
        foreach (KeyValuePair<int, String> kv in dt)
        {
            Console.WriteLine(kv.Key + " " + kv.Value);
        }
    }
}

2
zamiast jawnie przypisywać typ danych KeyValuePair, możemy użyć var. Zmniejszyłoby to pisanie - foreach (var kv in dt) ... tylko sugestia.
Ron

16

Słownik:

  • Zwraca / zgłasza wyjątek, jeśli próbujemy znaleźć klucz, który nie istnieje.

  • Jest szybszy niż Hashtable, ponieważ nie ma boksu i rozpakowywania.

  • Tylko publiczne elementy statyczne są bezpieczne dla wątków.

  • Słownik jest typem ogólnym, co oznacza, że ​​możemy go używać z dowolnym typem danych (podczas tworzenia należy określić typy danych zarówno dla kluczy, jak i wartości).

    Przykład: Dictionary<string, string> <NameOfDictionaryVar> = new Dictionary<string, string>();

  • Dictionay jest bezpieczną implementacją Hashtable Keysi Valuesjest mocno typowany.

Hashtable:

  • Zwraca null, jeśli próbujemy znaleźć klucz, który nie istnieje.

  • Jest wolniejszy niż słownik, ponieważ wymaga boksu i rozpakowania.

  • Wszyscy członkowie Hashtable są bezpieczni dla wątków,

  • Hashtable nie jest typem ogólnym,

  • Hashtable to luźno wpisana struktura danych, możemy dodawać klucze i wartości dowolnego typu.


„Zwraca / zgłasza wyjątek, jeśli próbujemy znaleźć klucz, który nie istnieje”. Nie, jeśli używaszDictionary.TryGetValue
Jim Balter

16

Wszechstronne badania struktur danych przy użyciu C # artykułu na MSDN stwierdza, że istnieje również różnica w strategii rozdzielczości kolizji :

Klasa Hashtable wykorzystuje technikę nazywaną powtórnym przetwarzaniem .

Rehashing działa w następujący sposób: istnieje zestaw różnych funkcji skrótu, H 1 ... H n , a podczas wstawiania lub pobierania elementu z tabeli skrótów początkowo używana jest funkcja skrótu H 1 . Jeśli ten prowadzi do kolizji, H 2 jest wypróbowany zamiast, i dalej aż do H n razie potrzeby.

Słownik wykorzystuje technikę zwaną łańcuchem .

W przypadku powtórzenia skrótu w przypadku kolizji skrót jest obliczany ponownie i wypróbowywana jest nowa szczelina odpowiadająca skrótowi. W przypadku łańcuchów wykorzystywana jest jednak wtórna struktura danych, która przechowuje wszelkie kolizje . W szczególności każde miejsce w Słowniku ma tablicę elementów, które są mapowane do tego segmentu. W przypadku kolizji element kolidujący jest dodawany do listy wiadra.


16

Od .NET Framework 3.5 istnieje również taki, HashSet<T>który zapewnia wszystkie zalety, Dictionary<TKey, TValue>jeśli potrzebujesz tylko kluczy i żadnych wartości.

Więc jeśli używasz a Dictionary<MyType, object>i zawsze ustawiasz wartość, aby nullsymulować bezpieczną tablicę skrótów, powinieneś rozważyć przejście na HashSet<T>.


14

HashtableJest luźno wpisane struktura danych, dzięki czemu można dodać klucze i wartości dowolnego typu do Hashtable. DictionaryKlasa jest typu bezpieczne Hashtablewdrożenie, a klucze i wartości są mocno wpisane. Podczas tworzenia Dictionaryinstancji należy określić typy danych zarówno dla klucza, jak i wartości.


11

Zauważ, że MSDN mówi: "Słownik <(z <(TKey, TValue>)>) Klasa jest zaimplementowany jako tabeli mieszania ", a nie "Dictionary <(z <(TKey, TValue>)>) Klasa jest zaimplementowany jako HashTable "

Słownik NIE jest zaimplementowany jako HashTable, ale jest implementowany zgodnie z koncepcją tabeli mieszającej. Implementacja nie jest powiązana z klasą HashTable z powodu użycia Generics, chociaż wewnętrznie Microsoft mógł użyć tego samego kodu i zastąpić symbole typu Object TKey i TValue.

W .NET 1.0 Generics nie istniał; to tutaj początkowo HashTable i ArrayList rozpoczęły się.


Czy możesz naprawić wycenę MSDN? Coś brakuje lub jest nie tak; nie jest gramatyczne i nieco niezrozumiałe.
Peter Mortensen

10

HashTable:

Klucz / wartość zostaną przekonwertowane na typ obiektu (boks) podczas przechowywania w stercie.

Klucz / wartość należy przekonwertować na pożądany typ podczas odczytu ze sterty.

Te operacje są bardzo kosztowne. Musimy unikać boksowania / rozpakowywania w jak największym stopniu.

Słownik: Ogólny wariant HashTable.

Bez boksowania / rozpakowywania. Nie wymaga konwersji.


8

Obiekt Hashtable składa się z segmentów zawierających elementy kolekcji. Wiadro to wirtualna podgrupa elementów w Hashtable, dzięki czemu wyszukiwanie i wyszukiwanie jest łatwiejsze i szybsze niż w większości kolekcji .

Klasa Dictionary ma taką samą funkcjonalność jak klasa Hashtable. Słownik określonego typu (inny niż Object) ma lepszą wydajność niż Hashtable dla typów wartości, ponieważ elementy Hashtable są typu Object, a zatem boksowanie i rozpakowywanie zwykle występuje podczas przechowywania lub pobierania typu wartości.

Do dalszej lektury: Hashtable i typy zbiorów słownikowych


7

Inną ważną różnicą jest to, że Hashtable jest bezpieczny dla wątków. Hashtable ma wbudowane zabezpieczenie wątku wielu czytników / pojedynczych pisarzy (MR / SW), co oznacza, że ​​Hashtable umożliwia JEDEN pisarzowi razem z wieloma czytnikami bez blokowania.

W przypadku słownika nie ma bezpieczeństwa wątków; jeśli potrzebujesz bezpieczeństwa wątków, musisz wdrożyć własną synchronizację.

Aby rozwinąć dalej:

Hashtable zapewnia pewne bezpieczeństwo wątków poprzez Synchronizedwłaściwość, która zwraca bezpieczne opakowanie do wątków wokół kolekcji. Opakowanie działa poprzez zablokowanie całej kolekcji przy każdej operacji dodawania lub usuwania. Dlatego każdy wątek, który próbuje uzyskać dostęp do kolekcji, musi czekać na swoją kolej, aby przejąć jedną blokadę. Nie jest to skalowalne i może powodować znaczne obniżenie wydajności dużych kolekcji. Ponadto projekt nie jest całkowicie chroniony przed warunkami wyścigowymi.

Klasy kolekcji .NET Framework 2.0, takie jak List<T>, Dictionary<TKey, TValue>itp., Nie zapewniają synchronizacji wątków; kod użytkownika musi zapewniać całą synchronizację, gdy elementy są dodawane lub usuwane jednocześnie w wielu wątkach

Jeśli potrzebujesz bezpieczeństwa typu oraz bezpieczeństwa wątków, użyj współbieżnych klas kolekcji w .NET Framework. Więcej informacji tutaj .

Dodatkową różnicą jest to, że po dodaniu wielu pozycji do słownika zachowana jest kolejność dodawania pozycji. Gdy odzyskamy elementy ze słownika, uzyskamy rekordy w tej samej kolejności, w jakiej je wstawiliśmy. Podczas gdy Hashtable nie zachowuje kolejności wstawiania.


Z tego, co rozumiem, Hashsetgwarancje bezpieczeństwa wątku MR / SW w scenariuszach użytkowania , które nie wymagają usunięcia . Wydaje mi się, że mogła być w pełni bezpieczna dla MR / SW, ale bezpieczne usuwanie usunięć znacznie zwiększa koszt bezpieczeństwa MR / SW. Chociaż projekt Dictionarymógł zaoferować bezpieczeństwo MR / SW przy minimalnych kosztach w scenariuszach bez usuwania, myślę, że MS chciało uniknąć traktowania scenariuszy bez usuwania jako „specjalnych”.
supercat

5

Jeszcze jedną różnicą, którą mogę zrozumieć, jest:

Nie możemy używać słownika <KT, VT> (ogólne) z usługami internetowymi. Powodem jest to, że żaden standard usług internetowych nie obsługuje standardu generycznego.


Możemy korzystać z list ogólnych (List <ciąg>) w serwisie internetowym na bazie mydła. Nie możemy jednak używać słownika (lub tablicy skrótów) w usłudze internetowej. Myślę, że powodem tego jest to, że xmlserializer .net nie może obsłużyć obiektu słownika.
Siddharth

5

Dictionary<> jest typem ogólnym, więc jest bezpieczny.

Możesz wstawić dowolny typ wartości do HashTable, co może czasami generować wyjątek. Ale Dictionary<int>akceptuje tylko wartości całkowite i podobnie Dictionary<string>akceptuje tylko łańcuchy.

Lepiej więc używać Dictionary<>zamiast HashTable.


0

W większości języków programowania preferowane są słowniki zamiast tablic skrótów

Nie sądzę, że jest to koniecznie prawda, większość języków ma jeden lub drugi, w zależności od preferowanej terminologii .

Jednak w C # oczywistym powodem (dla mnie) jest to, że C # HashTables i inni członkowie przestrzeni nazw System.Collections są w dużej mierze przestarzałe. Byli obecni w c # V1.1. Zostały one zastąpione z C # 2.0 przez klasy Generic w przestrzeni nazw System.Collections.Generic.


Jedną z zalet tablicy mieszającej nad słownikiem jest to, że jeśli klucz nie istnieje w słowniku, spowoduje to błąd. Jeśli klucz nie istnieje w tablicy mieszającej, zwraca po prostu null.
Bill Norman,

W języku C # nadal unikałbym używania System.Collections.Hashtable, ponieważ nie mają przewagi generycznych. Możesz użyć TryGetValue lub HasKey ze słownika, jeśli nie wiesz, czy klucz będzie istniał.
kristianp

Ups, nie HasKey, powinien być ContainsKey.
Kristianp

-3

Zgodnie z tym, co widzę za pomocą .NET Reflector :

[Serializable, ComVisible(true)]
public abstract class DictionaryBase : IDictionary, ICollection, IEnumerable
{
    // Fields
    private Hashtable hashtable;

    // Methods
    protected DictionaryBase();
    public void Clear();
.
.
.
}
Take note of these lines
// Fields
private Hashtable hashtable;

Możemy więc być pewni, że DictionaryBase wewnętrznie używa HashTable.


16
System.Collections.Generic.Dictionary <TKey, TValue> nie pochodzi od DictionaryBase.
snemarch

„Możemy więc być pewni, że DictionaryBase wewnętrznie używa tabeli HashTable”. - To miłe, ale nie ma to nic wspólnego z pytaniem.
Jim Balter
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.