Dlaczego tak wiele języków jest przekazywanych przez wartość?

36

Języki nawet gdzie masz wyraźnej manipulacji wskaźnik C jak to zawsze przekazywane przez wartość (ty może przekazać je poprzez odniesienie, ale nie jest to zachowanie domyślne).

Jaka jest z tego korzyść, dlaczego tak wiele języków jest przekazywanych przez wartości, a inne przez referencje ? (Rozumiem, że Haskell jest przekazywany przez referencję, choć nie jestem pewien).

Mój kod nie zawiera błędów
źródło
5
void acceptEntireProgrammingLanguageByValue(C++);
Thomas Eding,
4
Mogło być gorzej. Niektóre stare języki zezwalają również na dzwonienie po imieniu
hugomg,
25
W rzeczywistości w C nie można przejść przez odniesienie. Możesz przekazać wskaźnik według wartości , która jest bardzo podobna do przekazywania przez referencję, ale nie to samo. Jednak w C ++ można przekazać przez referencję.
Mason Wheeler,
@Mason Wheeler, możesz rozwinąć więcej lub dodać link, ponieważ twoje oświadczenie nie jest dla mnie jasne, ponieważ nie jestem guru C / C ++, jestem zwykłym programistą, dzięki
Betlista
1
@Betlista: Przekazując przez referencję, możesz napisać procedurę wymiany, która wygląda następująco: temp := a; a := b; b := temp;A kiedy ona zwróci, wartości ai bzostaną zamienione. Nie ma takiej możliwości w C; trzeba przejść do wskazówek ai bi swaprutyna musi działać na wartościach, do których prowadzą.
Mason Wheeler,

Odpowiedzi:

58

Przekazywanie przez wartość jest często bezpieczniejsze niż przekazywanie przez referencję, ponieważ nie można przypadkowo zmodyfikować parametrów metody / funkcji. Dzięki temu język jest prostszy w użyciu, ponieważ nie musisz się martwić o zmienne, które podajesz funkcji. Wiesz, że nie zostaną zmienione, a często tego się spodziewasz .

Jednakże, jeśli chcesz zmodyfikować parametry, musisz mieć wyraźną operację, aby to wyjaśnić (przekazać wskaźnik). Zmusi to wszystkich twoich rozmówców do wykonania połączenia nieco inaczej ( &variablew C), a to wyraźnie wyjaśni, że parametr zmiennej może zostać zmieniony.

Teraz możesz założyć, że funkcja nie zmieni twojego parametru zmiennej, chyba że jest to wyraźnie zaznaczone (wymagając od ciebie podania wskaźnika). Jest to bezpieczniejsze i czystsze rozwiązanie niż alternatywa: załóż, że wszystko może zmienić twoje parametry, chyba że wyraźnie powiedzą, że nie mogą.

Oleksi
źródło
4
Tablice +1 rozpadające się na wskaźniki stanowią znaczący wyjątek, ale ogólne wyjaśnienie jest dobre.
dasblinkenlight
6
@dasblinkenlight: tablice to bolesne miejsce w C :(
Matthieu M.,
55

Call-by-Value i Call-by-Reference to techniki implementacji, które dawno temu były mylone z trybami przekazywania parametrów.

Na początku istniał FORTRAN. FORTRAN miał tylko call-by-referencję, ponieważ podprogramy musiały być w stanie modyfikować swoje parametry, a cykle obliczeniowe były zbyt drogie, aby umożliwić wiele trybów przekazywania parametrów, a ponadto nie było wystarczająco dużo informacji o programowaniu, gdy FORTRAN został zdefiniowany po raz pierwszy.

ALGOL wymyślił nazwę po nazwie i nazwę po wartości. Call-by-value dotyczyło rzeczy, których nie należy zmieniać (parametry wejściowe). Call-by-name było dla parametrów wyjściowych. Call-by-name okazało się poważnym crockiem, a ALGOL 68 go upuścił.

PASCAL dostarczył funkcję call-by-value i call-by-reference. Nie dało to programiście żadnego sposobu na poinformowanie kompilatora, że ​​przekazuje duży obiekt (zwykle tablicę) przez odniesienie, aby uniknąć wysadzenia stosu parametrów, ale tego obiektu nie należy zmieniać.

PASCAL dodał wskaźniki do leksykonu projektowania języka.

C dostarczył call-by-value i symulował call-by-referencję poprzez zdefiniowanie operatora kludge w celu zwrócenia wskaźnika do dowolnego obiektu w pamięci.

Późniejsze języki kopiowały C, głównie dlatego, że projektanci nigdy nie widzieli nic innego. Prawdopodobnie dlatego tak popularna jest funkcja call-by-value.

C ++ dodał kludge na górze C kludge, aby zapewnić call-by-reference.

Teraz, jako bezpośredni rezultat call-by-value vs. call-by-reference vs. call-by-pointer-kludge, C i C ++ (programiści) mają straszne bóle głowy ze wskaźnikami const i wskaźnikami const (tylko do odczytu) przedmioty

Adzie udało się uniknąć tego koszmaru.

Ada nie ma wyraźnej funkcji call-by-value vs. call-by-reference. Ada ma raczej parametry wejściowe (które można odczytać, ale nie zapisane), parametry wyjściowe (które MUSZĄ zostać zapisane, zanim będą mogły zostać odczytane) oraz parametry wyjściowe, które można odczytać i zapisać w dowolnej kolejności. Kompilator decyduje, czy dany parametr jest przekazywany przez wartość, czy przez odniesienie: jest on przezroczysty dla programisty.

John R. Strohm
źródło
1
+1. To jedyna odpowiedź, jaką do tej pory widziałem, która faktycznie odpowiada na pytanie i ma sens i nie wykorzystuje go jako przejrzystej wymówki dla pro-FP.
Mason Wheeler,
11
+1 za „Późniejsze języki skopiowały C, głównie dlatego, że projektanci nigdy nie widzieli nic innego”. Za każdym razem, gdy widzę nowy język ze stałymi ósemkowymi z prefiksem 0, umieram trochę w środku.
librik
4
To może być pierwsze zdanie Biblii programowania: In the beginning, there was FORTRAN.
1
W odniesieniu do ADA, jeśli zmienna globalna zostanie przekazana do parametru wejścia / wyjścia, to czy język uniemożliwi zagnieżdżone wywołanie jego sprawdzeniu lub modyfikacji? Jeśli nie, to sądzę, że byłaby wyraźna różnica między parametrami wejścia / wyjścia i ref. Osobiście chciałbym, aby języki wyraźnie obsługiwały wszystkie kombinacje „wejścia / wyjścia / wejścia + wyjścia” i „według wartości / według odwołania / nieważności”, aby programiści mogli zapewnić maksymalną jasność co do ich zamiarów, ale optymalizatory miałby maksymalną elastyczność we wdrażaniu [pod warunkiem, że zakłócałby intencje programisty].
supercat
2
@ Neikos Istnieje również fakt, że 0prefiks jest niespójny z każdym innym prefiksem innym niż podstawowy. Może to być nieco usprawiedliwione w C (i w większości kompatybilnych ze źródłami językach, np. C ++, Objective C), jeśli octal był jedyną alternatywną bazą po wprowadzeniu, ale kiedy bardziej nowoczesne języki używają obu 0xi 0od samego początku, to po prostu sprawia, że ​​wyglądają źle wymyślony.
8bittree
13

Przekazywanie przez odniesienie pozwala na bardzo subtelne niezamierzone skutki uboczne, które są bardzo trudne do prawie niemożliwego do wykrycia, gdy zaczynają powodować niezamierzone zachowanie.

Przechodzą przez wartości, w szczególności final, staticczy constparametry wejściowe czyni całą tę klasę błędów zniknie.

Niezmienne języki są jeszcze bardziej deterministyczne i łatwiejsze do uzasadnienia i zrozumienia, co się dzieje i czego oczekuje się od funkcji.


źródło
7
Jest to jedna z tych rzeczy, które wymyśliłeś po próbie debugowania „starożytnego” kodu VB, gdzie wszystko jest domyślnie odniesieniem.
jfrankcarr
Może po prostu moje pochodzenie jest inne, ale nie mam pojęcia, o jakich „bardzo subtelnych niezamierzonych skutkach ubocznych, które są bardzo trudne lub prawie niemożliwe do wyśledzenia”, o których mówisz. Jestem przyzwyczajony do Pascala, gdzie wartość domyślna to wartość domyślna, ale można użyć odniesienia poprzez jawne oznaczenie parametru (w zasadzie ten sam model co C #) i nigdy nie miałem z tym problemu. Widzę, jak byłoby to problematyczne w „starożytnym VB”, w którym domyślnym jest by-ref, ale gdy jest on włączony, sprawia, że ​​myślisz o tym podczas pisania.
Mason Wheeler
4
@MasonWheeler co z nowicjuszami, którzy przychodzą za tobą i po prostu kopiują to, co zrobiłeś, nie rozumiejąc tego i pośrednio manipuluj tym stanem w dowolnym miejscu, cały świat dzieje się cały czas. Dobrą rzeczą jest mniejsza pośredniczość. Niezmienne jest najlepsze.
1
@MasonWheeler Proste przykłady aliasów, takie jak ataki Swaphakerskie, które nie używają zmiennej tymczasowej, choć same prawdopodobnie nie stanowią problemu, pokazują, jak trudny może być debugowanie bardziej złożonego przykładu.
Mark Hurd
1
@MasonWheeler: Klasyczny przykład w FORTRAN, który doprowadził do powiedzenia „Zmienne nie będą; stałe nie są”, przekazywałby stałą zmiennoprzecinkową, taką jak 3.0, do funkcji, która modyfikuje przekazany parametr. Dla każdej stałej zmiennoprzecinkowej, która została przekazana do funkcji, system utworzyłby „ukrytą” zmienną, która została zainicjowana z odpowiednią wartością i mogła być przekazana do funkcji. Jeśli funkcja dodała 1.0 do swojego parametru, dowolny podzbiór 3.0 w programie mógłby nagle stać się 4.0.
supercat
8

Dlaczego tak wiele języków jest przekazywanych przez wartość?

Istotą podziału dużych programów na małe podprogramy jest to, że możesz samodzielnie rozumować podprogramy. Przekazywanie przez odniesienie psuje tę właściwość. (Podobnie jak współdzielony stan mutable.)

Języki nawet gdzie masz wyraźnej manipulacji wskaźnik C jak to zawsze przekazywane przez wartość (ty może przekazać je poprzez odniesienie, ale nie jest to zachowanie domyślne).

W rzeczywistości C jest zawsze wartością przekazywaną, nigdy nie referencyjną. Możesz wziąć adres czegoś i przekazać ten adres, ale adres nadal będzie przekazywany przez wartość.

Jaka jest z tego korzyść, dlaczego tak wiele języków jest przekazywanych przez wartości, a inne przez referencje ?

Istnieją dwa główne powody, dla których warto skorzystać z metody przekazywania według odwołania:

  1. symulowanie wielu zwracanych wartości
  2. wydajność

Osobiście uważam, że nr 1 jest fałszywy: prawie zawsze jest usprawiedliwieniem złego interfejsu API i / lub języka:

  1. Jeśli potrzebujesz wielu zwracanych wartości, nie symuluj ich, po prostu użyj języka, który je obsługuje.
  2. Równie dobrze możesz symulować wiele zwracanych wartości, umieszczając je w lekkiej strukturze danych, takiej jak krotka. Działa to szczególnie dobrze, jeśli język obsługuje dopasowanie wzorca lub wiązanie destrukcyjne. Np. Ruby:

    def foo
      # This is actually just a single return value, an array: [1, 2, 3]
      return 1, 2, 3
    end
    
    # Ruby supports destructuring bind for arrays: a, b, c = [1, 2, 3]
    one, two, three = foo
    
  3. Często nie potrzebujesz nawet wielu zwracanych wartości. Na przykład popularnym wzorcem jest to, że podprogram zwraca kod błędu, a rzeczywisty wynik jest zapisywany przez odwołanie. Zamiast tego powinieneś po prostu rzucić wyjątek, jeśli błąd jest nieoczekiwany, lub zwrócić, Either<Exception, T>jeśli błąd jest oczekiwany. Innym wzorcem jest zwrócenie wartości logicznej, która informuje, czy operacja się powiodła, i zwrócenie rzeczywistego wyniku przez referencję. Ponownie, jeśli błąd jest nieoczekiwany, powinieneś zamiast tego zgłosić wyjątek, jeśli błąd jest oczekiwany, np. Podczas wyszukiwania wartości w słowniku, powinieneś Maybe<T>zamiast tego zwrócić wartość .

Przekazywanie przez odniesienie może być bardziej wydajne niż przekazywanie przez wartość, ponieważ nie trzeba kopiować wartości.

(Rozumiem, że Haskell jest przekazywany przez referencję, choć nie jestem pewien).

Nie, Haskell nie jest referencją. Nie jest to również wartość dodana. Przekazywanie przez odniesienie i przekazywanie według wartości są ścisłymi strategiami oceny, ale Haskell nie jest ścisły.

W rzeczywistości specyfikacja Haskell nie określa żadnej konkretnej strategii oceny. Większość implementacji Hakell korzysta z kombinacji imienia i nazwiska i imienia i nazwiska (wariant imienia i nazwiska z zapamiętywaniem), ale standard tego nie nakazuje.

Zauważ, że nawet w przypadku ścisłych języków nie ma sensu rozróżnianie między odniesieniami lub wartościami dla języków funkcjonalnych, ponieważ różnicę między nimi można zaobserwować tylko po zmutowaniu odwołania. Dlatego implementator może swobodnie wybierać między nimi, bez przerywania semantyki języka.

Jörg W Mittag
źródło
9
„Jeśli potrzebujesz wielu zwracanych wartości, nie symuluj ich, po prostu użyj języka, który je obsługuje”. To trochę dziwna rzecz do powiedzenia. Mówi w zasadzie „jeśli twój język potrafi zrobić wszystko, czego potrzebujesz, ale jednej funkcji, której prawdopodobnie będziesz potrzebować w mniej niż 1% kodu - ale nadal będziesz go potrzebował za ten 1% - nie da się zrobić w szczególnie czysty sposób, wtedy twój język nie jest wystarczająco dobry dla twojego projektu i powinieneś przepisać całość w innym języku. ” Przepraszam, ale to po prostu śmieszne.
Mason Wheeler
+1 Całkowicie zgadzam się z tym punktem . Rozbicie dużych programów na małe podprogramy polega na tym, że możesz podchodzić niezależnie do podprogramów. Przekazywanie przez odniesienie psuje tę właściwość. (Podobnie jak dzielny stan mutable.)
Remi
3

Istnieją różne zachowania w zależności od modelu wywołującego języka, rodzaju argumentów i modeli pamięci języka.

W przypadku prostych rodzimych typów przekazywanie według wartości pozwala przekazać wartość przez rejestry. Może to być bardzo szybkie, ponieważ wartości nie trzeba ładować z pamięci ani zapisywać. Podobna optymalizacja jest również możliwa po prostu przez ponowne użycie pamięci używanej przez argument po zakończeniu go przez odbiorcę, bez ryzyka zepsucia się z kopią obiektu wywołującego. Gdyby argument był obiektem tymczasowym, prawdopodobnie zapisałbyś w ten sposób pełną kopię (C ++ 11 czyni ten rodzaj optymalizacji jeszcze bardziej oczywistym dzięki nowemu prawemu odnośnikowi i jego semantycznemu ruchowi).

W wielu językach OO (C ++ jest bardziej wyjątkiem w tym kontekście), nie można przekazać obiektu przez wartość. Jesteś zmuszony przekazać to przez odniesienie. Dzięki temu kod jest domyślnie polimorficzny i jest bardziej zgodny z pojęciem instancji właściwym dla OO. Ponadto, jeśli chcesz przekazać wartość, musisz sam wykonać kopię, potwierdzając koszt wydajności, która wygenerowała takie działanie. W takim przypadku wybierz język, który ma większe szanse na najlepszą wydajność.

W przypadku języków funkcjonalnych myślę, że podanie wartości lub referencji jest po prostu kwestią optymalizacji. Ponieważ funkcje w takim języku są czyste i wolne od skutków ubocznych, naprawdę nie ma powodu, aby kopiować wartość, z wyjątkiem prędkości. Jestem nawet całkiem pewien, że taki język często dzieli tę samą kopię obiektów o tych samych wartościach, możliwość dostępna tylko wtedy, gdy użyjesz semantycznej referencji (const). Python użył również tej sztuczki do tworzenia liczb całkowitych i wspólnych (takich jak nazwy metod i klas), wyjaśniając, dlaczego liczby całkowite i ciągi są stałymi obiektami w Pythonie. Pomaga to również ponownie zoptymalizować kod, umożliwiając na przykład porównanie wskaźnika zamiast porównania zawartości i leniwą ocenę niektórych danych wewnętrznych.

Fabien Ninoles
źródło
2

Możesz przekazać wyrażenie według wartości, to naturalne. Przekazywanie wyrażenia przez (tymczasowe) odwołanie jest ... dziwne.

herby
źródło
1
Ponadto przekazanie wyrażenia przez tymczasowe odwołanie może prowadzić do złych błędów (w języku stanowym), gdy z radością je zmienisz (ponieważ jest to tylko tymczasowe), ale potem odwraca się, gdy faktycznie przekazujesz zmienną, i musisz opracować brzydkie obejście, takie jak przekazywanie foo +0 zamiast foo.
herby
2

Jeśli przejdziesz przez odniesienie, wtedy efektywnie zawsze będziesz pracować z wartościami globalnymi, ze wszystkimi problemami globalnymi (mianowicie zakresem i niezamierzonymi skutkami ubocznymi).

Referencje, podobnie jak globale, są czasem korzystne, ale nie powinny być twoim pierwszym wyborem.

jmoreno
źródło
3
Zakładam, że miałeś na myśli przekazanie referencji w pierwszym zdaniu?
jk.