Dlaczego obiekty są przekazywane przez odniesienie?


31

Młody współpracownik, który studiował OO, zapytał mnie, dlaczego każdy przedmiot jest przekazywany przez referencję, co jest przeciwieństwem prymitywnych typów lub struktur. Jest to wspólna cecha języków takich jak Java i C #.

Nie mogłem znaleźć dla niego dobrej odpowiedzi.

Jakie są motywy tej decyzji projektowej? Czy programiści tych języków byli zmęczeni tworzeniem wskaźników za każdym razem?


2
Czy pytasz, dlaczego Java i C # przekazujesz parametry przez referencję zamiast wartości lub referencję zamiast wskaźnika?
Robert

@Robert, czy na wysokim poziomie jest jakaś różnica między „referencją zamiast wskaźnikiem”? Czy uważasz, że powinienem zmienić tytuł na „dlaczego obiekty zawsze są referencjami?”?
Gustavo Cardoso,

Referencje są wskaźnikami.
compman

2
@Anto: Odwołanie Java jest pod każdym względem identyczne z prawidłowo używanym wskaźnikiem C (właściwie użyte: nie rzutuje tekstu, nie jest ustawione na niepoprawną pamięć, nie jest ustawione literałem).
Zan Lynx,

5
Żeby być naprawdę pedantycznym, tytuł jest niepoprawny (przynajmniej jeśli chodzi o .net). Obiekty NIE są przekazywane przez referencję, referencje są przekazywane przez wartość. Gdy przekazujesz obiekt do metody, wartość odniesienia jest kopiowana do nowego odwołania w treści metody. Myślę, że szkoda, że ​​„obiekty są przekazywane przez referencje” weszło na listę typowych cytatów programistycznych, gdy jest niepoprawne i prowadzi do gorszego zrozumienia referencji dla początkujących programistów.
SecretDeveloper

Odpowiedzi:


15

Podstawowe przyczyny sprowadzają się do tego:

  • Wskaźniki są techniczne, aby uzyskać poprawne
  • Potrzebujesz wskaźników do implementacji określonych struktur danych
  • Potrzebujesz wskaźników, aby efektywnie wykorzystywać pamięć
  • Nie potrzebujesz ręcznego indeksowania pamięci do pracy, jeśli nie korzystasz bezpośrednio ze sprzętu.

Stąd referencje.


32

Prosta odpowiedź:

Minimalizowanie zużycia pamięci
i
czasu procesora przy odtwarzaniu i wykonywaniu głębokiej kopii każdego obiektu, który gdzieś przeszedł.


Zgadzam się z tobą, ale myślę, że jest też motywacja estetyczna lub projektowa OO.
Gustavo Cardoso,

9
@Gustavo Cardoso: „motywacja estetyczna lub stylistyczna OO”. Nie. To po prostu optymalizacja.
S.Lott,

6
@ S.Lott: Nie, w kategoriach OO sensowne jest przekazywanie przez referencję, ponieważ semantycznie nie chcesz robić kopii obiektów. Chcesz przekazać obiekt, a nie jego kopię. Jeśli przechodzisz przez wartość, to nieco psuje metaforę OO, ponieważ masz wszystkie te klony obiektów generowane w całym miejscu, które nie mają sensu na wyższym poziomie.
intuicyjnie

@Gustavo: Myślę, że dyskutujemy o tym samym. Wspominasz semantykę OOP i odwołujesz się do metafory OOP, która jest dodatkowym powodem do mojego. Wydaje mi się, że twórcy OOP zrobili to tak, jak zrobili, aby „zminimalizować zużycie pamięci” i „Oszczędź czas procesora”
Tim

14

W C ++ masz dwie główne opcje: zwracaj przez wartość lub zwracaj przez wskaźnik. Spójrzmy na pierwszy:

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

Zakładając, że twój kompilator nie jest wystarczająco inteligentny, aby użyć optymalizacji wartości zwracanej, dzieje się tutaj:

  • newObj jest konstruowany jako obiekt tymczasowy i umieszczany na stosie lokalnym.
  • Kopia newObj jest tworzona i zwracana.

Zrobiliśmy kopię obiektu bez sensu. To strata czasu przetwarzania.

Zamiast tego spójrzmy na wskaźnik powrotu po wskaźniku:

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

Usunęliśmy zbędną kopię, ale teraz wprowadziliśmy kolejny problem: stworzyliśmy na stercie obiekt, który nie zostanie automatycznie zniszczony. Sami musimy sobie z tym poradzić:

MyClass someObj = getNewObject();
delete someObj;

Wiedza, kto jest odpowiedzialny za usunięcie obiektu przydzielonego w ten sposób, jest czymś, co można przekazać tylko za pomocą komentarzy lub konwencji. Łatwo prowadzi do wycieków pamięci.

Sugerowano wiele obejść w celu rozwiązania tych dwóch problemów - optymalizacja wartości zwracanej (w której kompilator jest wystarczająco inteligentny, aby nie tworzyć redundantnej kopii w wartości zwracanej przez wartość), przekazując odwołanie do metody (więc funkcja wstrzykuje do metody istniejący obiekt zamiast tworzenia nowego), inteligentne wskaźniki (tak, że kwestia własności jest dyskusyjna).

Twórcy Java / C # zdali sobie sprawę, że zawsze zwracanie obiektu przez referencję jest lepszym rozwiązaniem, szczególnie jeśli język obsługuje go natywnie. Łączy się z wieloma innymi funkcjami języków, takimi jak wyrzucanie elementów bezużytecznych itp.


Wartość zwracana przez wartość jest wystarczająco zła, ale wartość przekazywana przez wartość jest jeszcze gorsza, jeśli chodzi o obiekty, i myślę, że to był prawdziwy problem, którego próbowali uniknąć.
Mason Wheeler,

na pewno masz ważny punkt. Ale problem projektowania OO, na który zwrócił uwagę Mason, był ostateczną motywacją zmiany. Nie było sensu utrzymywać różnicy między referencją a wartością, gdy chcesz po prostu użyć referencji.
Gustavo Cardoso,

10

Wiele innych odpowiedzi ma dobre informacje. Chciałbym dodać jeden ważny punkt dotyczący klonowania, który został tylko częściowo rozwiązany.

Używanie referencji jest mądre. Kopiowanie rzeczy jest niebezpieczne.

Jak powiedzieli inni, w Javie nie ma naturalnego „klonu”. To nie tylko brakująca funkcja. Nigdy nie chcesz po prostu nie chcąc * kopiować (płytko lub głęboko) każdej właściwości w obiekcie. Co jeśli ta właściwość była połączeniem z bazą danych? Nie można po prostu „sklonować” połączenia z bazą danych, tak jak nie można sklonować człowieka. Inicjalizacja istnieje z jakiegoś powodu.

Głębokie kopie same w sobie stanowią problem - jak głęboko naprawdę się posuniesz? Na pewno nie można skopiować niczego, co jest statyczne (w tym żadnych Classobiektów).

Z tego samego powodu, dla którego nie ma naturalnego klonu, przedmioty przekazywane jako kopie stworzyłyby obłęd . Nawet gdybyś mógł „sklonować” połączenie DB - jak byś teraz upewniał się, że jest ono zamknięte?


* Patrz komentarze - Przez to „nigdy” oświadczenie rozumiem automatyczny klon, który klonuje każdą właściwość. Java nie dostarczyła go i prawdopodobnie nie jest to dobry pomysł dla Ciebie jako użytkownika języka z powodów wymienionych tutaj. Klonowanie tylko nieprzemijających pól byłoby początkiem, ale nawet wtedy trzeba by być ostrożnym przy określaniu, transientgdzie jest to właściwe.


Mam problem ze zrozumieniem przejścia od dobrych zastrzeżeń do klonowania w określonych warunkach do stwierdzenia, że nigdy nie jest potrzebne. A ja spotkałem sytuacji, w których dokładną kopią było potrzebne, gdzie żadne funkcje statyczne, gdzie zaangażowane, nie IO lub otwartych połączeń może być rozpatrywany ... Rozumiem ryzyko klonowania, ale nie widzę koc nigdy .
Inca

2
@Inca - Możesz mnie źle zrozumieć. Celowe wdrożenie clonejest w porządku. Przez „willy-nilly” mam na myśli kopiowanie wszystkich właściwości bez myślenia o tym - bez celowego zamiaru. Projektanci języka Java wymusili tę intencję, wymagając od użytkownika implementacji clone.
Nicole,

Używanie odniesień do niezmiennych obiektów jest sprytne. Nie można modyfikować prostych wartości, takich jak Date, a następnie tworzyć wiele odwołań do nich.
kevin cline

@NickC: Główny powód, dla którego „klonowanie rzeczy nie chce” jest niebezpieczne, ponieważ języki / frameworki, takie jak Java i .net, nie mają żadnego sposobu na zadeklarowanie, czy odniesienie zawiera stan zmienny, tożsamość, oba, czy żadne. Jeśli pole zawiera odwołanie do obiektu, które zawiera stan zmienny, ale nie tożsamość, klonowanie obiektu wymaga, aby obiekt zawierający ten stan był powielony, a odniesienie do tego duplikatu przechowywane w polu. Jeśli odwołanie zawiera tożsamość, ale nie można go zmienić, pole w kopii musi odnosić się do tego samego obiektu, co w oryginale.
supercat

Ważnym aspektem jest aspekt głębokiej kopii. Kopiowanie obiektów jest problematyczne, gdy zawierają odniesienia do innych obiektów, szczególnie jeśli wykres obiektów zawiera obiekty zmienne.
Caleb

4

Obiekty są zawsze przywoływane w Javie. Nigdy nie są omijani.

Zaletą jest to, że upraszcza to język. Obiekt C ++ może być reprezentowany jako wartość lub odwołanie, co powoduje konieczność użycia dwóch różnych operatorów w celu uzyskania dostępu do członka: .i ->. (Istnieją powody, dla których nie można tego skonsolidować; na przykład inteligentne wskaźniki to wartości, które są referencjami i muszą je rozróżniać.) Java potrzebuje tylko ..

Innym powodem jest to, że polimorfizm musi być wykonywany przez odniesienie, a nie wartość; obiekt traktowany przez wartość jest właśnie tam i ma ustalony typ. Można to zepsuć w C ++.

Ponadto Java może zmienić domyślne przypisanie / kopiowanie / cokolwiek. W C ++ jest to mniej lub bardziej głęboka kopia, podczas gdy w Javie jest to proste przypisanie wskaźnika / kopiowanie / cokolwiek, z .clone()i tak na wypadek, gdybyś musiał skopiować.


Czasami robi się naprawdę brzydki, gdy używasz „(* obiekt) ->”
Gustavo Cardoso

1
Warto zauważyć, że C ++ rozróżnia wskaźniki, referencje i wartości. SomeClass * jest wskaźnikiem do obiektu. SomeClass & to odniesienie do obiektu. SomeClass jest typem wartości.
Ant

Już zadałem @Rober pytanie wstępne, ale zrobię to również tutaj: różnica między * a C ++ to tylko kwestia niskiego poziomu technicznego, prawda? Czy są na wysokim poziomie, semantycznie, są takie same.
Gustavo Cardoso,

3
@Gustavo Cardoso: Różnica jest semantyczna; na niskim poziomie technicznym są na ogół identyczne. Wskaźnik wskazuje na obiekt lub ma wartość NULL (zdefiniowana zła wartość). O ile nie constmożna zmienić jego wartości, aby wskazywała na inne obiekty. Odwołanie to inna nazwa obiektu, nie może mieć wartości NULL i nie może być ponownie ustawione. Zasadniczo jest implementowany przez proste użycie wskaźników, ale jest to szczegół implementacji.
David Thornley,

+1 za „polimorfizm musi być wykonany przez odniesienie”. To niezwykle ważny szczegół, który większość innych odpowiedzi zignorowała.
Doval

4

Twoje początkowe stwierdzenie o obiektach C # przekazywanych przez odwołanie jest niepoprawne. W języku C # obiekty są typami referencyjnymi, ale domyślnie są przekazywane przez wartość, podobnie jak typy wartości. W przypadku typu referencyjnego „wartość” kopiowana jako parametr metody pass-by-value jest samą referencją, więc zmiany właściwości wewnątrz metody zostaną odzwierciedlone poza zakresem metody.

Jeśli jednak ponownie przypiszesz zmienną parametru do metody, zobaczysz, że zmiana ta nie zostanie odzwierciedlona poza zakresem metody. W przeciwieństwie do tego, jeśli faktycznie przekazujesz parametr przez odwołanie za pomocą refsłowa kluczowego, to zachowanie działa zgodnie z oczekiwaniami.


3

Szybka odpowiedź

Projektanci języków Java i podobnych chcieli zastosować koncepcję „wszystko jest przedmiotem”. Przekazywanie danych jako referencji jest bardzo szybkie i nie zajmuje dużo pamięci.

Dodatkowy, nudny komentarz

Poza tym te języki używają odwołań do obiektów (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), prawda jest taka, że ​​odwołania do obiektów są wskaźnikami do ukrytych obiektów. Wartość null, przydział pamięci, kopia odwołania bez kopiowania całych danych obiektu, wszystkie są wskaźnikami obiektów, a nie zwykłymi obiektami !!!

W Object Pascal (nie Delphi), anc C ++ (nie Java, nie C #), obiekt może być zadeklarowany jako statycznie przydzielona zmienna, a także za pomocą dynamicznie przydzielonej zmiennej, poprzez użycie wskaźnika („odwołanie do obiektu” bez „ składnia cukru ”). Każdy przypadek używa określonej składni i nie ma sposobu, aby się pomylić, jak w Javie „i przyjaciele”. W tych językach obiekt można przekazać zarówno jako wartość, jak i jako odniesienie.

Programista wie, kiedy wymagana jest składnia wskaźnika, a kiedy nie jest wymagana, ale w Javie i podobnych językach jest to mylące.

Zanim Java istniała lub stała się głównym nurtem, wielu programistów nauczyło się OO w C ++ bez wskaźników, przekazując wartość lub referencję, gdy jest to wymagane. W przypadku przełączania z aplikacji do nauki na aplikacje biznesowe zwykle używają wskaźników obiektów. Biblioteka QT jest tego dobrym przykładem.

Kiedy nauczyłem się języka Java, starałem się podążać za koncepcją „wszystko jest przedmiotem”, ale pomyślałem o kodowaniu. W końcu powiedziałem „ok, to są obiekty przydzielane dynamicznie za pomocą wskaźnika ze składnią obiektu przydzielonego statycznie” i znowu nie miałem problemów z kodowaniem.


3

Java i C # przejmują kontrolę nad pamięcią niskiego poziomu. „Kupa”, w której znajdują się tworzone przez ciebie przedmioty, żyje własnym życiem; na przykład śmieciarz zbiera obiekty, kiedy tylko zechce.

Ponieważ istnieje oddzielna warstwa pośrednicząca między twoim programem a tą „stertą”, dwa sposoby odwoływania się do obiektu, według wartości i wskaźnika (jak w C ++), stają się nierozróżnialne : zawsze odwołujesz się do obiektów „wskaźnikiem” do gdzieś w kupie. Właśnie dlatego takie podejście projektowe sprawia, że ​​przekazywanie przez odniesienie jest domyślną semantyką przypisania. Java, C #, Ruby i tak dalej.

Powyższe dotyczy tylko języków imperatywnych. We wspomnianych językach kontrola nad pamięcią jest przekazywana do środowiska wykonawczego, ale projekt języka mówi również „hej, ale tak naprawdę jest pamięć i obiekty, które zajmują pamięć”. Języki funkcjonalne są jeszcze bardziej abstrakcyjne, wykluczając z definicji pojęcie „pamięci”. Właśnie dlatego przekazywanie przez odniesienie niekoniecznie dotyczy wszystkich języków, w których nie kontrolujesz pamięci niskiego poziomu.


2

Mogę wymyślić kilka powodów:

  • Kopiowanie typów pierwotnych jest banalne, zwykle przekłada się na jedną instrukcję maszynową.

  • Kopiowanie obiektów nie jest trywialne, obiekt może zawierać elementy, które same są obiektami. Kopiowanie obiektów jest kosztowne pod względem czasu procesora i pamięci. Istnieje nawet wiele sposobów kopiowania obiektu w zależności od kontekstu.

  • Przekazywanie obiektów przez odniesienie jest tanie i przydaje się również, gdy chcesz udostępnić / zaktualizować informacje o obiekcie między wieloma klientami obiektu.

  • Złożone struktury danych (szczególnie te rekurencyjne) wymagają wskaźników. Przekazywanie obiektów przez odniesienie jest po prostu bezpieczniejszym sposobem przekazywania wskaźników.


1

Ponieważ w przeciwnym razie funkcja powinna być w stanie automatycznie utworzyć (oczywiście głęboką) kopię dowolnego obiektu, który zostanie do niej przekazany. I zwykle nie da się tego zgadnąć. Musisz więc zdefiniować implementację metody kopiowania / klonowania dla wszystkich swoich obiektów / klas.


Czy może po prostu zrobić płytką kopię i zachować rzeczywiste wartości i wskaźniki do innych obiektów?
Gustavo Cardoso,

#Gustavo Cardoso, a następnie możesz modyfikować inne obiekty za pomocą tego, czy można oczekiwać od obiektu NIE przekazanego jako odniesienie?
David

0

Ponieważ Java została zaprojektowana jako lepszy C ++, a C # został zaprojektowany jako lepszy Java, a twórcy tych języków byli zmęczeni fundamentalnie zepsutym modelem obiektowym C ++, w którym obiekty są typami wartości.

Dwie z trzech podstawowych zasad programowania obiektowego to dziedziczenie i polimorfizm, a traktowanie obiektów jako typów wartości zamiast typów odniesienia powoduje spustoszenie w obu przypadkach. Gdy przekazujesz obiekt do funkcji jako parametr, kompilator musi wiedzieć, ile bajtów przekazać. Kiedy twój obiekt jest typem odniesienia, odpowiedź jest prosta: rozmiar wskaźnika, taki sam dla wszystkich obiektów. Ale gdy obiekt jest typem wartości, musi przekazać rzeczywisty rozmiar wartości. Ponieważ klasa pochodna może dodawać nowe pola, oznacza to rozmiarof (pochodna)! = Rozmiarof (podstawa), a polimorfizm wychodzi poza okno.

Oto prosty program w C ++, który demonstruje problem:

#include <iostream> 
class Parent 
{ 
public: 
   int a;
   int b;
   int c;
   Parent(int ia, int ib, int ic) { 
      a = ia; b = ib; c = ic;
   };
   virtual void doSomething(void) { 
      std::cout << "Parent doSomething" << std::endl;
   }
};

class Child : public Parent {
public:
   int d;
   int e;
   Child(int id, int ie) : Parent(1,2,3) { 
      d = id; e = ie;
   };
   virtual void doSomething(void) {
      std::cout << "Child doSomething : D = " << d << std::endl;
   }
};

void foo(Parent a) {
   a.doSomething();
}

int main(void)
{
   Child c(4, 5);
   foo(c);
   return 0;
}

Dane wyjściowe tego programu nie są takie same, jak w przypadku równoważnego programu w żadnym rozsądnym języku OO, ponieważ nie można przekazać obiektu pochodnego przez wartość do funkcji oczekującej obiektu podstawowego, więc kompilator tworzy konstruktor ukrytej kopii i przekazuje kopia nadrzędnej części obiektu podrzędnego , zamiast przekazywania obiektu podrzędnego, tak jak kazałeś to zrobić. Ukryte semantyczne błędy takie jak to, dlatego w C ++ należy unikać przekazywania obiektów według wartości i nie jest to możliwe w prawie każdym innym języku OO.


Bardzo dobra uwaga. Skupiłem się jednak na problemach z powrotami, ponieważ ich obejście wymaga sporo wysiłku; ten program można naprawić dodając pojedynczy znak ampersand: void foo (Parent & a)
Ant

OO naprawdę nie działa dobrze bez wskaźników
Gustavo Cardoso

-1.000000000000
P Shved

5
Ważne jest, aby pamiętać, że Java jest przekazywana według wartości (przekazuje odwołania do obiektów według wartości, a operacje podstawowe są przekazywane wyłącznie według wartości).
Nicole,

3
@Pavel Shved - „dwa są lepsze niż jeden!” Lub innymi słowy, więcej liny do powieszenia.
Nicole,

0

Ponieważ inaczej nie byłoby polimorfizmu.

W OO Programming możesz utworzyć większą Derivedklasę z Basejednej, a następnie przekazać ją do funkcji oczekujących Basejednej. Całkiem trywialne prawda?

Tyle że rozmiar argumentu funkcji jest stały i ustalany w czasie kompilacji. Możesz dyskutować, ile chcesz, kod wykonywalny jest taki, a języki muszą być wykonywane w jednym lub innym miejscu (języki czysto interpretowane nie są przez to ograniczone ...)

Teraz jest jeden kawałek danych, który jest dobrze zdefiniowany na komputerze: adres komórki pamięci, zwykle wyrażony jako jedno lub dwa „słowa”. Jest widoczny jako wskaźniki lub odniesienia w językach programowania.

Aby więc przekazać obiekty o dowolnej długości, najprostszą rzeczą jest przekazanie wskaźnika / odwołania do tego obiektu.

Jest to techniczne ograniczenie programowania OO.

Ale ponieważ w przypadku dużych typów i tak zazwyczaj wolisz przekazywać referencje, aby uniknąć kopiowania, nie jest to ogólnie uważane za poważny cios :)

Istnieje jedna ważna konsekwencja, w języku Java lub C #, gdy przekazujesz obiekt do metody, nie masz pojęcia, czy Twój obiekt zostanie zmodyfikowany za pomocą tej metody, czy nie. Sprawia to, że debugowanie / paralelizacja jest trudniejsza, i to jest problem, którym próbują się zająć języki funkcjonalne i transparentne referencje -> kopiowanie nie jest takie złe (jeśli ma to sens).


-1

Odpowiedź jest w nazwie (i tak prawie). Odwołanie (jak adres) po prostu odnosi się do czegoś innego, wartość jest kolejną kopią czegoś innego. Jestem pewien, że ktoś prawdopodobnie wspomniał coś o następującym skutku, ale będą okoliczności, w których jedno, a nie drugie będzie odpowiednie (bezpieczeństwo pamięci vs. wydajność pamięci). Chodzi o zarządzanie pamięcią, pamięcią, pamięcią ... PAMIĘĆ! :RE


-2

W porządku, więc nie mówię, że właśnie dlatego obiekty są typami referencji lub są przekazywane przez referencje, ale mogę podać przykład, dlaczego jest to bardzo dobry pomysł na dłuższą metę.

Jeśli się nie mylę, kiedy dziedziczysz klasę w C ++, wszystkie metody i właściwości tej klasy są fizycznie kopiowane do klasy potomnej. To tak, jakby pisać zawartość tej klasy ponownie w klasie podrzędnej.

Oznacza to, że całkowity rozmiar danych w klasie podrzędnej jest kombinacją elementów w klasie nadrzędnej i klasie pochodnej.

EG: #include

class Top 
{   
    int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Middle : Top 
{   
    int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Bottom : Middle
{   
    int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

int main()
{   
    using namespace std;

    int arr[20];
    cout << "Size of array of 20 ints: " << sizeof(arr) << endl;

    Top top;
    Middle middle;
    Bottom bottom;

    cout << "Size of Top Class: " << sizeof(top) << endl;
    cout << "Size of middle Class: " << sizeof(middle) << endl;
    cout << "Size of bottom Class: " << sizeof(bottom) << endl;

}   

Co pokaże ci:

Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240

Oznacza to, że jeśli masz dużą hierarchię kilku klas, zadeklarowany tutaj całkowity rozmiar obiektu będzie kombinacją wszystkich tych klas. Oczywiście w wielu przypadkach obiekty te byłyby znacznie większe.

Wierzę, że rozwiązaniem jest utworzenie go na stercie i użycie wskaźników. Oznacza to, że w pewnym sensie można zarządzać rozmiarem obiektów klas z kilkoma rodzicami.

Dlatego używanie referencji byłoby bardziej preferowaną metodą do tego.


1
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami przedstawionymi i wyjaśnionymi w 13 wcześniejszych odpowiedziach
gnat
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.