Jaka jest różnica między odwołaniem C # a wskaźnikiem?

85

Nie do końca rozumiem różnicę między odwołaniem C # a wskaźnikiem. Oboje wskazują na miejsce w pamięci, prawda? Jedyną różnicą, jaką mogę zrozumieć, jest to, że wskaźniki nie są tak sprytne, nie mogą wskazywać niczego na stercie, są wyłączone z czyszczenia pamięci i mogą odnosić się tylko do struktur lub typów podstawowych.

Jednym z powodów, o które pytam, jest przekonanie, że ludzie muszą dobrze rozumieć wskazówki (chyba z C), aby być dobrym programistą. Wiele osób, które uczą się języków na wyższym poziomie, tęskni za tym i dlatego ma tę słabość.

Po prostu nie rozumiem, co jest takiego złożonego we wskaźniku? Jest to po prostu odniesienie do miejsca w pamięci, prawda? Może zwrócić swoją lokalizację i bezpośrednio wchodzić w interakcję z obiektem w tej lokalizacji?

Czy przegapiłem ważny punkt?

Richard
źródło
1
Krótka odpowiedź brzmi: tak, przeoczyłeś coś istotnego i to jest powód „... przekonania, że ​​ludzie muszą rozumieć wskazówki”. Wskazówka: C # nie jest jedynym językiem na rynku.
jdigital

Odpowiedzi:

50

Odwołania w języku C # mogą i zostaną przeniesione przez moduł wyrzucania elementów bezużytecznych, ale normalne wskaźniki są statyczne. Dlatego używamy fixedsłowa kluczowego podczas pozyskiwania wskaźnika do elementu tablicy, aby zapobiec jego przeniesieniu.

EDYCJA: Koncepcyjnie tak. Są mniej więcej takie same.

Mehrdad Afshari
źródło
Czy nie ma innego polecenia, które uniemożliwia odwołanie C # przeniesienie obiektu, do którego odwołuje się, przez GC?
Richard
Och, przepraszam, myślałem, że to coś innego, ponieważ post odnosi się do wskaźnika.
Richard
Tak, GCHandle.Alloc lub Marshal.AllocHGlobal (poza ustalonymi)
ctacke
Naprawiono to w C #, pin_ptr w C ++ / CLI
Mehrdad Afshari
Marshal.AllocHGlobal w ogóle nie przydzieli pamięci w zarządzanym stercie i oczywiście nie podlega wyrzucaniu elementów bezużytecznych.
Mehrdad Afshari
130

Istnieje niewielkie, ale niezwykle ważne, rozróżnienie między wskaźnikiem a odniesieniem. Wskaźnik wskazuje miejsce w pamięci, podczas gdy odniesienie wskazuje na obiekt w pamięci. Wskaźniki nie są „bezpieczne dla typów” w tym sensie, że nie można zagwarantować poprawności pamięci, na którą wskazują.

Weźmy na przykład następujący kod

int* p1 = GetAPointer();

Jest to typ bezpieczny w tym sensie, że GetAPointer musi zwracać typ zgodny z int *. Jednak nadal nie ma gwarancji, że * p1 faktycznie wskaże int. Może to być znak, podwójny lub po prostu wskaźnik do pamięci losowej.

Odniesienie wskazuje jednak na konkretny obiekt. Obiekty można przenosić w pamięci, ale odwołanie nie może zostać unieważnione (chyba że używasz niebezpiecznego kodu). Odnośniki są pod tym względem dużo bezpieczniejsze niż wskazówki.

string str = GetAString();

W tym przypadku str ma jeden z dwóch stanów 1) nie wskazuje na żaden obiekt i dlatego jest pusty lub 2) wskazuje na prawidłowy łańcuch. Otóż ​​to. CLR gwarantuje, że tak jest. Nie może i nie będzie dla wskaźnika.

JaredPar
źródło
14
Świetne wyjaśnienie.
iTayb,
13

Odniesienie to „abstrakcyjny” wskaźnik: nie możesz wykonywać arytmetyki z odniesieniem i nie możesz wykonywać żadnych niskopoziomowych sztuczek z jego wartością.

Chris Conway
źródło
8

Główną różnicą między referencją a wskaźnikiem jest to, że wskaźnik jest zbiorem bitów, których zawartość ma znaczenie tylko wtedy, gdy jest aktywnie używany jako wskaźnik, podczas gdy odniesienie zawiera nie tylko zestaw bitów, ale także niektóre metadane, które zachowują podstawowe ramy poinformowane o jego istnieniu. Jeśli wskaźnik istnieje do jakiegoś obiektu w pamięci i ten obiekt jest usuwany, ale wskaźnik nie jest usuwany, dalsze istnienie wskaźnika nie spowoduje żadnych szkód, chyba że zostanie podjęta próba uzyskania dostępu do pamięci, na którą wskazuje. Jeśli nie podejmie się próby użycia wskaźnika, nic nie będzie dbać o jego istnienie. Z kolei struktury oparte na odwołaniach, takie jak .NET lub JVM, wymagają, aby system zawsze mógł zidentyfikować każde istniejące odniesienie do obiektu, a każde istniejące odwołanie do obiektu musi być zawszenull lub też zidentyfikuj obiekt odpowiedniego typu.

Należy zauważyć, że każde odwołanie do obiektu w rzeczywistości zawiera dwa rodzaje informacji: (1) zawartość pola obiektu, który identyfikuje, oraz (2) zestaw innych odniesień do tego samego obiektu. Chociaż nie ma żadnego mechanizmu, dzięki któremu system może szybko zidentyfikować wszystkie istniejące odniesienia do obiektu, zestaw innych odniesień, które istnieją do obiektu, może często być najważniejszą rzeczą hermetyzowaną przez odniesienie (jest to szczególnie prawdziwe, gdy rzeczy typu Objectsą używane jako rzeczy takie jak żetony zamków). Chociaż system przechowuje kilka bitów danych dla każdego obiektu do wykorzystania w programie GetHashCode, obiekty nie mają rzeczywistej tożsamości poza zestawem odniesień, które do nich istnieją. Jeśli Xzawiera jedyne istniejące odniesienie do obiektu, zastępowanieXodniesienie do nowego obiektu z taką samą zawartością pola nie będzie miało żadnego możliwego do zidentyfikowania efektu poza zmianą bitów zwracanych przezGetHashCode(), a nawet ten efekt nie jest gwarantowany.

supercat
źródło
5

Wskaźniki wskazują lokalizację w przestrzeni adresowej pamięci. Odnośniki wskazują na strukturę danych. Wszystkie struktury danych były przenoszone przez cały czas (no cóż, nie tak często, ale od czasu do czasu) przez garbage collector (w celu zagęszczenia pamięci). Ponadto, jak powiedziałeś, struktury danych bez odniesień zostaną po pewnym czasie zebrane jako śmieci.

Ponadto wskaźników można używać tylko w niebezpiecznym kontekście.

Tamas Czinege
źródło
5

Uważam, że ważne jest, aby programiści rozumieli pojęcie wskaźnika - to znaczy rozumieli pośrednictwo. Nie oznacza to, że muszą koniecznie używać wskaźników. Ważne jest również, aby zrozumieć, że koncepcja odwołania różni się od pojęcia wskaźnika , chociaż tylko nieznacznie, ale implementacja odwołania prawie zawsze jest wskaźnikiem.

Oznacza to, że zmienna przechowująca odniesienie jest po prostu blokiem pamięci wielkości wskaźnika, który zawiera wskaźnik do obiektu. Jednak tej zmiennej nie można używać w taki sam sposób, jak można używać zmiennej wskaźnikowej. W C # (oraz C i C ++, ...) wskaźnik może być indeksowany jak tablica, ale odwołanie nie może. W C # odwołanie jest śledzone przez moduł wyrzucania elementów bezużytecznych, wskaźnik nie może być. W C ++ można ponownie przypisać wskaźnik, a odwołanie nie. Składniowo i semantycznie wskaźniki i odwołania są całkiem różne, ale mechanicznie są takie same.

P tato
źródło
Rzecz z tablicą brzmi interesująco, czy w zasadzie gdzie można powiedzieć wskaźnikowi, aby przesunął lokalizację pamięci jak tablica, podczas gdy nie można tego zrobić z referencją? Nie mogę pomyśleć, kiedy byłoby to przydatne, ale mimo to interesujące.
Richard
Jeśli p to int * (wskaźnik do int), to (p + 1) jest adresem identyfikowanym przez p + 4 bajty (rozmiar int). A p [1] jest tym samym, co * (p + 1) (to znaczy „wyłuskuje” adres 4 bajty po p). Natomiast w przypadku odwołania do tablicy (w C #) operator [] wykonuje wywołanie funkcji.
P Daddy
5

Najpierw myślę, że musisz zdefiniować „wskaźnik” w swojej sematyce. Czy masz na myśli wskaźnik, który możesz utworzyć w niebezpiecznym kodzie z naprawionym ? Czy masz na myśli IntPtr , który otrzymujesz może z połączenia lokalnego lub Marshal.AllocHGlobal ? Czy masz na myśli GCHandle ? Wszystkie są zasadniczo tym samym - reprezentacją adresu pamięci, w którym coś jest przechowywane - czy to klasa, liczba, struktura, cokolwiek. I dla przypomnienia, z pewnością mogą być na stercie.

Wskaźnik (wszystkie powyższe wersje) jest stałym elementem. GC nie ma pojęcia, co znajduje się pod tym adresem i dlatego nie ma możliwości zarządzania pamięcią ani życiem obiektu. Oznacza to, że tracisz wszystkie korzyści płynące z systemu zbierania śmieci. Musisz ręcznie zarządzać pamięcią obiektu i istnieje ryzyko wycieków.

Z drugiej strony odniesienie to właściwie „zarządzany wskaźnik”, o którym wie GC. Wciąż jest to adres obiektu, ale teraz GC zna szczegóły celu, więc może go przesuwać, wykonywać kompresje, finalizować, usuwać i wszystkie inne fajne rzeczy, które wykonuje zarządzane środowisko.

Tak naprawdę główna różnica polega na tym, jak i dlaczego ich używasz. W zdecydowanej większości przypadków w języku zarządzanym będziesz używać odwołania do obiektu. Wskaźniki stają się przydatne do robienia interopów i rzadkiej potrzeby naprawdę szybkiej pracy.

Edycja: W rzeczywistości jest to dobry przykład, kiedy można użyć „wskaźnika” w kodzie zarządzanym - w tym przypadku jest to GCHandle, ale dokładnie to samo można było zrobić z AllocHGlobal lub używając ustalonej tablicy bajtów lub struktury. Wolę GCHandle, ponieważ wydaje mi się bardziej „.NET”.

ctacke
źródło
Drobny spór, że być może nie powinieneś tu mówić „zarządzany wskaźnik” - nawet z przerażającymi cudzysłowami - ponieważ jest to coś zupełnie innego niż odniesienie do obiektu w IL. Chociaż istnieje składnia zarządzanych wskaźników w C ++ / CLI, ogólnie nie są one dostępne z C #. W IL uzyskuje się je za pomocą instrukcji (tj.) Ldloca i ldarga.
Glenn Slayden,
5

Wskaźnik może wskazywać na dowolny bajt w przestrzeni adresowej aplikacji. Odwołanie jest ściśle ograniczone, kontrolowane i zarządzane przez środowisko .NET.

jdigital
źródło
1

To, co sprawia, że ​​wskaźniki są nieco skomplikowane, nie polega na tym, czym są, ale w tym, co można z nimi zrobić. A kiedy masz wskaźnik do wskaźnika do wskaźnika. Wtedy naprawdę zaczyna się bawić.

Robert C. Barth
źródło
1

Jedną z największych zalet odniesień w stosunku do wskaźników jest większa prostota i czytelność. Jak zawsze, gdy upraszczasz coś, ułatwiasz korzystanie, ale kosztem elastyczności i kontroli, którą uzyskujesz dzięki rzeczom niskiego poziomu (jak wspominali inni).

Wskaźniki są często krytykowane za to, że są „brzydkie”.

class* myClass = new class();

Teraz za każdym razem, gdy go używasz, musisz najpierw go usunąć

myClass->Method() or (*myClass).Method()

Pomimo utraty czytelności i dodania złożoności, ludzie nadal musieli często używać wskaźników jako parametrów, aby można było modyfikować rzeczywisty obiekt (zamiast przekazywać wartość) i aby zwiększyć wydajność bez konieczności kopiowania dużych obiektów.

Dla mnie właśnie dlatego „narodziły się” odniesienia, aby zapewnić te same korzyści co wskaźniki, ale bez całej tej składni wskaźników. Teraz możesz przekazać rzeczywisty obiekt (nie tylko jego wartość) ORAZ masz bardziej czytelny, normalny sposób interakcji z obiektem.

MyMethod(&type parameter)
{
   parameter.DoThis()
   parameter.DoThat()
}

Referencje C ++ różniły się od odwołań C # / Java tym, że po przypisaniu do nich wartości to było to, nie można było jej ponownie przypisać (i musi zostać przypisane, gdy zostało zadeklarowane). To było to samo, co użycie wskaźnika do stałej (wskaźnika, którego nie można było ponownie wskazać na inny obiekt).

Java i C # to bardzo zaawansowane, nowoczesne języki, które usuwały wiele bałaganu, który nagromadził się w C / C ++ przez lata, a wskaźniki były zdecydowanie jedną z tych rzeczy, które należało „wyczyścić”.

Jeśli twój komentarz na temat znajomości wskaźników czyni cię silniejszym programistą, jest to prawdą w większości przypadków. Jeśli wiesz, „jak” coś działa, w przeciwieństwie do zwykłego używania tego bez wiedzy, powiedziałbym, że często może to dać ci przewagę. Jak duża przewaga zawsze będzie się zmieniać. W końcu używanie czegoś bez wiedzy, jak jest zaimplementowane, jest jednym z wielu uroków OOP i interfejsów.

W tym konkretnym przykładzie, co wiedza o wskaźnikach pomogłaby w znalezieniu odniesień? Zrozumienie, że odwołanie w C # NIE jest samym obiektem, ale wskazuje na obiekt, jest bardzo ważną koncepcją.

# 1: NIE przekazujesz wartości Cóż, na początek, kiedy używasz wskaźnika, wiesz, że wskaźnik zawiera tylko adres, to wszystko. Sama zmienna jest prawie pusta i dlatego tak miło jest przekazywać jako argumenty. Oprócz zwiększenia wydajności pracujesz z rzeczywistym obiektem, więc wszelkie wprowadzone zmiany nie są tymczasowe

# 2: Polimorfizm / interfejsy Kiedy masz odniesienie, które jest typem interfejsu i wskazuje na obiekt, możesz wywoływać tylko metody tego interfejsu, nawet jeśli obiekt może mieć znacznie więcej możliwości. Obiekty mogą też inaczej implementować te same metody.

Jeśli dobrze rozumiesz te koncepcje, to nie sądzę, że tracisz zbyt wiele z powodu nieużywania wskaźników. C ++ jest często używany jako język do nauki programowania, ponieważ czasami dobrze jest ubrudzić sobie ręce. Ponadto praca z aspektami niższego poziomu sprawia, że ​​doceniasz wygodę nowoczesnego języka. Zacząłem od C ++, a teraz jestem programistą C # i czuję, że praca z surowymi wskaźnikami pomogła mi lepiej zrozumieć, co się dzieje pod maską.

Nie sądzę, aby każdy musiał zaczynać od wskaźników, ale ważne jest, aby rozumieli, dlaczego zamiast typów wartości używa się odwołań, a najlepszym sposobem na zrozumienie tego jest przyjrzenie się jego przodkowi, wskaźnikowi.

Despertar
źródło
1
Osobiście uważam, że C # byłoby lepiej język, jeśli większość miejsc, które wykorzystują .stosowane ->, ale foo.bar(123)było równoznaczne z wywołaniem metody statycznej fooClass.bar(ref foo, 123). To pozwoliłoby na takie rzeczy myString.Append("George"); [co zmieniłoby zmienną myString ] i uwydatniło różnicę w znaczeniu między myStruct.field = 3;a myClassObject->field = 3;.
supercat