Czy w moich metodach należy akceptować puste kolekcje, które się nad nimi powtarzają?


40

Mam metodę, w której cała logika jest wykonywana w pętli foreach, która iteruje nad parametrem metody:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

W takim przypadku wysłanie pustej kolekcji skutkuje pustą kolekcją, ale zastanawiam się, czy to nierozsądne.

Moja logika polega na tym, że jeśli ktoś wywołuje tę metodę, to zamierza przekazać dane i przekazałby pustą kolekcję mojej metodzie tylko w błędnych okolicznościach.

Czy powinienem wychwycić to zachowanie i zgłosić wyjątek, czy też najlepszą praktyką jest zwrócenie pustej kolekcji?


43
Czy na pewno twoje założenie „jeśli ktoś wywołuje tę metodę, a następnie zamierza przekazać dane” jest prawidłowe? Może po prostu przekazują to, co mają pod ręką, aby pracować z rezultatem.
SpaceTrucker

7
BTW: twoja metoda „transformacji” jest zwykle nazywana „mapą”, chociaż w .NET (i SQL, skąd nazwa .NET pochodzi od nazwy), nazywa się ją zwykle „select”. „Transform” to, co nazywa to C ++, ale nie jest to powszechna nazwa, którą można rozpoznać.
Jörg W Mittag

25
Rzucałbym, jeśli kolekcja jest, nullale nie, jeśli jest pusta.
CodesInChaos

21
@NickUdell - cały czas przekazuję puste kolekcje w kodzie. Kodowi łatwiej jest zachowywać się tak samo, jak pisać specjalną logikę rozgałęziania dla przypadków, w których nie mogłem nic dodać do mojej kolekcji przed przejściem do niej.
maja

7
Jeśli sprawdzisz i wyrzucisz błąd, jeśli kolekcja jest pusta, zmuszasz swojego rozmówcę do sprawdzenia i nie przekazania pustej kolekcji do metody. Sprawdzanie poprawności powinno odbywać się na (i tylko na) granicach systemu, jeśli przeszkadzają ci puste kolekcje, powinieneś już je sprawdzić na długo przed osiągnięciem jakichkolwiek metod użyteczności. Teraz masz dwa zbędne czeki.
Lie Ryan,

Odpowiedzi:


175

Metody użytkowe nie powinny rzucać pustych kolekcji. Twoi klienci API nienawidzą cię za to.

Kolekcja może być pusta; „kolekcja-która-nie może być pusta” jest koncepcyjnie znacznie trudniejsza do pracy.

Przekształcenie pustej kolekcji ma oczywisty skutek: pusta kolekcja. (Możesz nawet zaoszczędzić trochę śmieci, zwracając sam parametr.)

Istnieje wiele okoliczności, w których moduł przechowuje listy rzeczy, które mogą, ale nie muszą być już czymś wypełnione. Konieczność sprawdzania pustki przed każdym wezwaniem do transformjest denerwująca i może przekształcić prosty, elegancki algorytm w brzydki bałagan.

Metody użyteczności powinny zawsze dążyć do tego, aby ich wkład był liberalny, a wyniki - konserwatywne.

Z tych wszystkich powodów, na miłość boską, postępuj właściwie z pustą kolekcją. Nic nie jest bardziej irytujące niż moduł pomocnika, który myśli, że wie, czego chcesz lepiej niż ty.


14
+1 - (więcej gdybym mógł). Mogę nawet posunąć się tak daleko, że zleję się również nullw pustą kolekcję. Funkcje z ukrytymi ograniczeniami są uciążliwe.
Telastyn

6
„Nie, nie powinieneś”… nie powinieneś ich zaakceptować czy też nie powinien wyrzucić wyjątku?
Ben Aaronson

16
@Andy - nie byłyby to jednak metody użytkowe . Byłyby to metody biznesowe lub podobne konkretne zachowanie.
Telastyn

38
Nie sądzę, aby recykling pustej kolekcji do zwrotu był dobrym pomysłem. Oszczędzasz tylko odrobinę pamięci; i może to spowodować nieoczekiwane zachowanie osoby dzwoniącej. Nieco wymyślony przykład: Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); jeśli Populate1 opuści kolekcję pustą i zwrócisz ją dla pierwszego wywołania TransformNodes, wyjście1 i dane wejściowe będą tą samą kolekcją, a gdy Populaten2 zostanie wywołany, jeśli umieści węzły w kolekcji, skończysz na drugim zestaw danych wejściowych w wyjściu 2.
Dan Neely

6
@Andy Nawet więc, jeśli argument nigdy nie powinien być pusty - jeśli jest to błąd nie mieć dane do procesu - wtedy czuję, że to w gestii rozmówcy to sprawdzić, kiedy jest zestawianie ten argument. Nie tylko sprawia, że ​​ta metoda jest bardziej elegancka, ale oznacza, że ​​można wygenerować lepszy komunikat o błędzie (lepszy kontekst) i podnieść go w szybki sposób. Klasa nie ma sensu weryfikować niezmienników dzwoniącego ...
Andrzej Doyle

26

Widzę dwa ważne pytania, które determinują odpowiedź na to:

  1. Czy twoja funkcja może zwrócić coś sensownego i logicznego po przekazaniu pustej kolekcji (w tym null )?
  2. Jaki jest ogólny styl programowania w tej aplikacji / bibliotece / zespole? (W szczególności, jak się masz FP?)

1. Znaczący zwrot

Zasadniczo, jeśli możesz zwrócić coś znaczącego, nie rzucaj wyjątku. Pozwól dzwoniącemu poradzić sobie z wynikiem. Więc jeśli twoja funkcja ...

  • Liczy liczbę elementów w kolekcji, zwraca 0. To było łatwe.
  • Wyszukuje elementy spełniające określone kryteria, zwraca pustą kolekcję. Proszę nic nie rzucać. Dzwoniący może mieć ogromną kolekcję kolekcji, z których niektóre są puste, a niektóre nie. Dzwoniący chce dowolnych pasujących elementów w dowolnej kolekcji. Wyjątki tylko utrudniają życie dzwoniącemu.
  • Szuka największych / najmniejszych / najlepiej dopasowanych do kryteriów na liście. Ups W zależności od pytania o styl możesz tutaj rzucić wyjątek lub możesz zwrócić wartość null . Nienawidzę null (w dużej mierze osoby FP), ale jest to prawdopodobnie bardziej znaczące tutaj i pozwala zarezerwować wyjątki na nieoczekiwane błędy w twoim własnym kodzie. Jeśli dzwoniący nie sprawdzi wartości zerowej, i tak powstanie dość wyraźny wyjątek. Ale zostaw to im.
  • Pyta o n-ty element w kolekcji lub o pierwszy / ostatni n elementów. Jest to najlepszy jak dotąd wyjątek i ten, który w najmniejszym stopniu może powodować zamieszanie i trudności dla dzwoniącego. Sprawa nadal może zostać złożona na wartość zerową, jeśli ty i twój zespół jesteście przyzwyczajeni do sprawdzania tego, ze wszystkich powodów podanych w poprzednim punkcie, ale jest to najbardziej uzasadniony jak dotąd przypadek zgłoszenia wyjątku DudeYou Know YouShouldCheckTheSizeFirst . Jeśli twój styl jest bardziej funkcjonalny, albo zeruj, albo czytaj dalej odpowiedź na mój styl .

Ogólnie rzecz biorąc, moje uprzedzenie do FP mówi mi „zwróć coś znaczącego”, a null może mieć w tym przypadku ważne znaczenie.

2. Styl

Czy Twój ogólny kod (lub kod projektu lub kod zespołu) faworyzuje styl funkcjonalny? Jeśli nie, należy spodziewać się wyjątków i zająć się nimi. Jeśli tak, rozważ zwrócenie typu opcji . W przypadku typu opcji zwracana jest znacząca odpowiedź lub Brak / Nic . W moim trzecim przykładzie z góry Nic nie byłoby dobrą odpowiedzią w stylu FP. Fakt, że funkcja zwraca typ opcji wyraźnie dzwoniącemu niż sensowna odpowiedź, może nie być możliwy i osoba dzwoniąca powinna być na to przygotowana. Wydaje mi się, że daje to dzwoniącemu więcej opcji (jeśli wybaczysz kalamburowi).

F # jest gdzie wszystkie fajne .Net dzieci robić tego typu rzeczy, ale C # ma wspierać ten styl.

tl; dr

Zachowaj wyjątki dla nieoczekiwanych błędów we własnej ścieżce kodu, nie do końca przewidywalnych (i legalnych) danych wejściowych pochodzących od kogoś innego.


„Najlepsze dopasowanie” itp .: Być może istnieje metoda, która znajdzie obiekt najlepszego dopasowania w kolekcji spełniającej określone kryteria. Ta metoda miałaby ten sam problem w przypadku niepustych kolekcji, ponieważ nic nie pasowałoby do kryterium, więc nie trzeba się martwić o puste wybory.
gnasher729,

1
Nie, dlatego powiedziałem „najlepsze dopasowanie”, a nie „dopasowanie”; wyrażenie oznacza najbliższe przybliżenie, a nie dokładne dopasowanie. Największe / najmniejsze opcje powinny być wskazówką. Jeśli poproszono tylko o „najlepsze dopasowanie”, wówczas każda kolekcja z co najmniej 1 członkiem powinna mieć możliwość zwrócenia członka. Jeśli jest tylko jeden członek, jest to najlepsze dopasowanie.
itsbruce

1
Zwrot „null” w większości przypadków jest gorszy niż zgłoszenie wyjątku. Zwracanie pustej kolekcji ma jednak znaczenie.
Ian

1
Nie, jeśli musisz zwrócić pojedynczy przedmiot (min / max / głowa / ogon). Co do null, jak powiedziałem, nienawidzę go (i wolę języki, które go nie mają), ale tam, gdzie język go ma, musisz być w stanie sobie z tym poradzić i kod, który go rozdaje. W zależności od lokalnych konwencji kodowania może być właściwe.
itsbruce

Żaden z twoich przykładów nie ma nic wspólnego z pustym wejściem. To tylko szczególne przypadki sytuacji, w których odpowiedź może brzmieć „nic”. Co z kolei powinno odpowiedzieć na pytanie PO dotyczące „obsługi” pustej kolekcji.
djechlin

18

Jak zawsze to zależy.

Czy to ważne, że kolekcja jest pusta?
Większość kodu obsługi kolekcji prawdopodobnie powiedziałaby „nie”; kolekcja może zawierać dowolną liczbę elementów, w tym zero.

Teraz, jeśli masz jakąś kolekcję, w której „nieważne” jest to, że nie ma w niej żadnych przedmiotów, jest to nowy wymóg i musisz zdecydować, co z tym zrobić.

Pożycz trochę logiki testowania ze świata bazy danych: test na zero przedmiotów, jeden przedmiot i dwa przedmioty. Dotyczy to najważniejszych przypadków (spłukanie źle sformułowanych wewnętrznych lub kartezjańskich warunków łączenia).


A jeśli nie ma żadnej pozycji w kolekcji, idealnie byłoby, gdyby argumentem java.util.Collectionbyła com.foo.util.NonEmptyCollectionklasa niestandardowa , która może konsekwentnie zachowywać ten niezmiennik i uniemożliwić na początku stan nieprawidłowy.
Andrzej Doyle

3
To pytanie jest oznaczone [c #]
Nick Udell

@NickUdell nie obsługuje C # programowania obiektowego lub koncepcji zagnieżdżonych przestrzeni nazw? TIL
John Dvorak

2
To robi. Mój komentarz był wyjaśnieniem, ponieważ Andrzej musiał być zdezorientowany (bo po co mieliby starać się określić przestrzeń nazw dla języka, którego to pytanie nie dotyczy?).
Nick Udell

-1 za podkreślenie „to zależy” bardziej niż „prawie na pewno tak”. Bez żartów - przekazanie niewłaściwej rzeczy powoduje tutaj uszkodzenie.
djechlin

11

W ramach dobrego projektu zaakceptuj jak najwięcej zmian w danych wejściowych, tak praktycznych, jak to możliwe. Wyjątki powinny być zgłaszane tylko wtedy, gdy (niedopuszczalne dane wejściowe są prezentowane LUB wystąpią nieoczekiwane błędy podczas przetwarzania) ORAZ program nie może kontynuować w przewidywalny sposób .

W takim przypadku należy się spodziewać, że zostanie zaprezentowana pusta kolekcja, a Twój kod musi ją obsłużyć (co już robi). Byłoby naruszeniem wszystkiego, co dobre, gdyby Twój kod zgłosił tutaj wyjątek. Byłoby to podobne do mnożenia 0 przez 0 w matematyce. Jest zbędny, ale absolutnie niezbędny, aby działał tak, jak działa.

Teraz przejdźmy do argumentu zerowej kolekcji. W tym przypadku kolekcja zerowa jest błędem programistycznym: programista zapomniał przypisać zmienną. Jest to przypadek, w którym wyjątek może zostać zgłoszony, ponieważ nie można w sposób znaczący przetworzyć go w dane wyjściowe, a próba zrobienia tego spowodowałaby nieoczekiwane zachowanie. Byłoby to podobne do dzielenia przez zero w matematyce - jest to całkowicie bez znaczenia.


Twoje śmiałe stwierdzenie to bardzo zła rada w pełnej ogólności. Myślę, że to prawda, ale musisz wyjaśnić, co jest wyjątkowego w tej sytuacji. (Ogólnie jest to zła rada, ponieważ sprzyja cichym awariom.)
djechlin

@AAA - najwyraźniej nie piszemy tutaj książki o tym, jak projektować oprogramowanie, ale nie uważam, że to zła rada, jeśli naprawdę rozumiesz, co mówię. Chodzi mi o to, że jeśli złe dane wejściowe generują niejednoznaczne lub arbitralne dane wyjściowe, musisz zgłosić wyjątek. W przypadkach, w których dane wyjściowe są całkowicie przewidywalne (jak w tym przypadku), wówczas zgłoszenie wyjątku byłoby arbitralne. Kluczem jest, aby nigdy nie podejmować arbitralnych decyzji w swoim programie, ponieważ stąd bierze się nieprzewidziane zachowanie.
maja

Ale to twoje pierwsze zdanie i jedyne wyróżnione ... nikt jeszcze nie rozumie, co mówisz. (Nie staram się być tutaj zbyt agresywny, tylko jedną z rzeczy, które tu jesteśmy, jest nauka pisania i komunikowania się, a robię to, że utrata precyzji w kluczowym punkcie jest szkodliwa.)
djechlin

10

Właściwe rozwiązanie jest znacznie trudniejsze do zauważenia, gdy patrzysz tylko na swoją funkcję w izolacji. Rozważ swoją funkcję jako część większego problemu . Jedno z możliwych rozwiązań tego przykładu wygląda następująco (w Scali):

input.split("\\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

Najpierw dzielisz ciąg znaków na nie cyfrowy, odfiltrowujesz puste ciągi, konwertujesz ciągi na liczby całkowite, a następnie filtrujesz, aby zachować tylko liczby czterocyfrowe. Twoja funkcja może być map (_.toInt)w przygotowaniu.

Ten kod jest dość prosty, ponieważ każdy etap w potoku obsługuje tylko pusty ciąg lub pustą kolekcję. Jeśli umieścisz pusty ciąg na początku, otrzymasz pustą listę na końcu. Nie musisz się zatrzymywać i sprawdzać nullani wyjątku po każdym połączeniu.

Oczywiście, zakładając, że pusta lista wyników nie ma więcej niż jednego znaczenia. Jeśli musisz odróżnić puste wyjście spowodowane pustym wejściem od tego spowodowane przez samą transformację, to całkowicie to zmienia.


+1 za niezwykle skuteczny przykład (brak innych dobrych odpowiedzi).
djechlin

2

To pytanie dotyczy w rzeczywistości wyjątków. Jeśli spojrzysz na to w ten sposób i zignorujesz pustą kolekcję jako szczegół implementacji, odpowiedź jest prosta:

1) Metoda powinna zgłosić wyjątek, gdy nie może kontynuować: albo nie może wykonać wyznaczonego zadania, albo zwrócić odpowiednią wartość.

2) Metoda powinna wychwycić wyjątek, gdy jest w stanie kontynuować pomimo niepowodzenia.

Zatem twoja metoda pomocnicza nie powinna być „pomocna” i rzucać wyjątek, chyba że nie jest w stanie wykonać swojej pracy z pustą kolekcją. Pozwól dzwoniącemu ustalić, czy wyniki mogą być obsługiwane.

Niezależnie od tego, czy zwraca pustą kolekcję, czy ma wartość NULL, jest to nieco trudniejsze, ale niewiele: należy w miarę możliwości unikać kolekcji zerowalnych. Celem zbioru zerowalnego byłoby wskazanie (jak w SQL), że nie masz informacji - na przykład zbiór dzieci może być zerowy, jeśli nie wiesz, czy ktoś go posiada, ale nie masz wiedzą, że nie. Ale jeśli jest to ważne z jakiegoś powodu, prawdopodobnie warto go śledzić.


1

Nazwa metody TransformNodes. W przypadku pustej kolekcji jako danych wejściowych odzyskanie pustej kolekcji jest naturalne i intuicyjne i ma sens matematyczny.

Jeśli metoda została nazwana Maxi zaprojektowana tak, aby zwracała maksymalną liczbę elementów, naturalnym byłoby wrzucenie NoSuchElementExceptionpustej kolekcji, ponieważ maksimum niczego nie ma sensu matematycznego.

Jeśli metoda została nazwana JoinSqlColumnNamesi zaprojektowana w taki sposób, aby zwracała ciąg znaków, do którego elementy są połączone przecinkiem w celu użycia w zapytaniach SQL, wówczas sensownym byłoby wrzucenie IllegalArgumentExceptionpustej kolekcji, ponieważ wywołujący i tak otrzymałby błąd SQL, gdyby użył ciąg w zapytaniu SQL bezpośrednio bez dalszych sprawdzeń, a zamiast sprawdzania zwróconego pustego ciągu naprawdę powinien był sprawdzić, czy nie ma pustej kolekcji.


Maxniczego jest często ujemną nieskończonością.
djechlin

0

Cofnijmy się i skorzystajmy z innego przykładu, który oblicza średnią arytmetyczną tablicy wartości.

Jeśli tablica wejściowa jest pusta (lub zerowa), czy możesz w uzasadniony sposób spełnić żądanie osoby dzwoniącej? Nie. Jakie masz opcje? Cóż, możesz:

  • przedstawić / zwrócić / wyrzucić błąd. używając konwencji bazy kodu dla tej klasy błędów.
  • dokument, że zostanie zwrócona wartość taka jak zero
  • dokument, że wyznaczona niepoprawna wartość zostanie zwrócona (np. NaN)
  • dokument, że zostanie zwrócona magiczna wartość (np. min. lub maks. dla danego typu lub jakaś wartość, która może być wskazująca)
  • ogłosić, że wynik nie jest określony
  • zadeklaruj, że akcja jest niezdefiniowana
  • itp.

Mówię, że daj im błąd, jeśli podali ci nieprawidłowe dane wejściowe, a prośba nie może zostać zrealizowana. Mam na myśli poważny błąd od pierwszego dnia, aby zrozumieli wymagania twojego programu. W końcu twoja funkcja nie jest w stanie odpowiedzieć. Jeśli operacja może się nie powieść (np. Skopiować plik), wówczas Twój interfejs API powinien dać im błąd, z którym mogą sobie poradzić.

Dzięki temu można zdefiniować sposób obsługi źle sformułowanych żądań i żądań, które mogą się nie powieść.

Bardzo ważne jest, aby kod był spójny w zakresie obsługi tych klas błędów.

Kolejną kategorią jest decyzja, w jaki sposób Twoja biblioteka będzie obsługiwać nonsensowne żądania. Wracając do przykładu podobny do Ciebie - użyjmy funkcję, która określa, czy plik istnieje w ścieżce: bool FileExistsAtPath(String). Jeśli klient przekaże pusty ciąg, jak poradzić sobie z tym scenariuszem? Co powiesz na pustą lub zerową tablicę przekazaną void SaveDocuments(Array<Document>)? Wybierz bibliotekę / bazę kodów i zachowaj spójność. Zdarza mi się brać pod uwagę te przypadki błędów i zabraniam klientom wysuwania bzdurnych żądań, oznaczając je jako błędy (poprzez asercję). Niektórzy ludzie zdecydowanie oprą się temu pomysłowi / działaniu. Uważam, że to wykrywanie błędów jest bardzo pomocne. Jest bardzo dobry do lokalizowania problemów w programach - z dobrą lokalizacją dla szkodliwego programu. Programy są znacznie bardziej przejrzyste i poprawne (rozważ ewolucję bazy kodu) i nie nagrywaj cykli w ramach funkcji, które nic nie robią. Kod jest mniejszy / czystszy w ten sposób, a kontrole są generalnie wypychane do miejsc, w których problem może zostać wprowadzony.


6
W ostatnim przykładzie nie jest zadaniem tej funkcji ustalanie, co jest nonsensem. Dlatego pusty ciąg powinien zwracać wartość false. Rzeczywiście, właśnie takie jest podejście File.Exists .
maja

@ rmayer06 to jest jego praca. funkcja, do której się odwołuje, zezwala na zerowe, puste ciągi, sprawdza nieprawidłowe znaki w ciągu, wykonuje obcinanie łańcucha, może wymagać wykonywania wywołań systemu plików, może wymagać zapytania do środowiska itp. te często nadmiarowe w trakcie wykonywania „udogodnienia” mają wysokie koszty i zdecydowanie zacierają linie poprawności (IMO). widziałem wiele if (!FileExists(path)) { ...create it!!!... }błędów - z których wiele można było złapać przed zatwierdzeniem, gdyby nie zostały zamazane linie poprawności.
justin

2
Zgodziłbym się z tobą, gdyby nazwa funkcji brzmiała File.ThrowExceptionIfPathIsInvalid, ale kto przy zdrowych zmysłach nazwałby taką funkcję?
May

Średnio 0 elementów już zwróci NaN lub wyrzuci wyjątek dzielenia przez zero, tylko ze względu na to, jak funkcja jest zdefiniowana. Plik o nazwie, która nie może istnieć, albo nie będzie istnieć, albo już spowoduje błąd. Nie ma ważnego powodu, aby zajmować się tymi sprawami specjalnie.
cHao

Można nawet argumentować, że cała koncepcja sprawdzania istnienia pliku przed jego odczytaniem lub zapisaniem jest wadliwa. (Jest to nieodłączny warunek wyścigu.) Lepiej po prostu idź dalej i spróbuj otworzyć plik, do czytania lub z ustawieniami tylko do tworzenia, jeśli piszesz. To już się nie powiedzie, jeśli nazwa pliku jest nieprawidłowa lub nie istnieje (lub tak jest, w przypadku zapisu).
cHao

-3

Zasadniczo standardowa funkcja powinna być w stanie zaakceptować jak najszerszą listę danych wejściowych i przekazać informacje zwrotne na jej temat, istnieje wiele przykładów, w których programiści używają funkcji w sposób, który nie został zaplanowany przez projektanta, dlatego też uważam, że funkcja powinien być w stanie zaakceptować nie tylko puste kolekcje, ale szeroki zakres typów danych wejściowych i wdzięcznie zwracać informacje zwrotne, czy to nawet obiekt błędu z jakiejkolwiek operacji wykonanej na wejściu ...


Absolutnie błędne jest założenie, że kolekcja zawsze zostanie przekazana i przejdzie bezpośrednio do iteracji po tej kolekcji, należy sprawdzić, czy otrzymany argument jest zgodny z oczekiwanym typem danych ...
Clement Mark-Aaba

3
„szeroki zakres typów danych wejściowych” wydaje się tutaj nieistotny. Pytanie jest oznaczone jako c # , język silnie napisany. Oznacza to, że kompilator gwarantuje, że dane wejściowe są kolekcją
gnat

Uprzejmie „ogólna zasada” Google, moją odpowiedzią była ogólna uwaga, że ​​jako programista robisz miejsce na nieprzewidziane lub błędne zdarzenia, jednym z nich mogą być błędnie przekazane argumenty funkcyjne ...
Clement Mark-Aaba

1
Jest to niewłaściwe lub tautologiczne. writePaycheckToEmployeenie powinien przyjmować liczby ujemnej jako danych wejściowych ... ale jeśli „sprzężenie zwrotne” oznacza „robi coś, co może się zdarzyć później”, to tak, każda funkcja zrobi coś, co zrobi dalej.
djechlin
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.