В чем разница между ссылкой на C # и указателем?

86

Я не совсем понимаю разницу между ссылкой на C # и указателем. Они оба указывают на место в памяти, не так ли? Единственное различие, которое я могу понять, заключается в том, что указатели не так умны, не могут указывать ни на что в куче, не подлежат сборке мусора и могут ссылаться только на структуры или базовые типы.

Одна из причин, по которой я спрашиваю, заключается в том, что существует мнение, что людям нужно хорошо понимать указатели (полагаю, с языка C), чтобы быть хорошим программистом. Многие люди, изучающие языки более высокого уровня, упускают это и поэтому имеют эту слабость.

Я просто не понимаю, что такого сложного в указателе? По сути, это просто ссылка на место в памяти, не так ли? Он может возвращать свое местоположение и напрямую взаимодействовать с объектом в этом месте?

Я упустил важный момент?

Ричард
источник
1
Короткий ответ - да, вы упустили что-то достаточно важное, и это причина «... мнения, что люди должны понимать указатели». Подсказка: C # - не единственный язык.
jdigital

Ответы:

51

Ссылки C # могут и будут перемещены сборщиком мусора, но обычные указатели статичны. Вот почему мы используем fixedключевое слово при получении указателя на элемент массива, чтобы предотвратить его перемещение.

РЕДАКТИРОВАТЬ: концептуально да. Они более-менее похожи.

ммх
источник
Нет ли другой команды, которая предотвращает перемещение ссылки C # объекта, на который она ссылается, GC?
Ричард
Ой, извините, я подумал, что это что-то еще, потому что сообщение ссылалось на указатель.
Ричард
Да, GCHandle.Alloc или Marshal.AllocHGlobal (сверх установленного)
ctacke
Это исправлено в C #, pin_ptr в C ++ / CLI
mmx
Marshal.AllocHGlobal вообще не выделяет память в управляемой куче и, естественно, не подлежит сборке мусора.
mmx
133

Между указателем и ссылкой есть небольшое, но чрезвычайно важное различие. Указатель указывает на место в памяти, а ссылка указывает на объект в памяти. Указатели не являются «типобезопасными» в том смысле, что вы не можете гарантировать правильность памяти, на которую они указывают.

Возьмем, например, следующий код

int* p1 = GetAPointer();

Это типобезопасный в том смысле, что GetAPointer должен возвращать тип, совместимый с int *. Тем не менее, все еще нет гарантии, что * p1 действительно будет указывать на int. Это может быть char, double или просто указатель на случайную память.

Однако ссылка указывает на конкретный объект. Объекты можно перемещать в памяти, но ссылка не может быть признана недействительной (если вы не используете небезопасный код). Ссылки в этом отношении намного безопаснее, чем указатели.

string str = GetAString();

В этом случае str имеет одно из двух состояний: 1) он не указывает ни на один объект и, следовательно, равен нулю, или 2) он указывает на допустимую строку. Вот и все. CLR гарантирует, что это так. Это не может и не будет для указателя.

ДжаредПар
источник
13

Ссылка - это «абстрактный» указатель: вы не можете выполнять арифметические операции со ссылкой, и вы не можете использовать какие-либо низкоуровневые трюки с ее значением.

Крис Конвей
источник
8

Основное различие между ссылкой и указателем заключается в том, что указатель представляет собой набор битов, содержимое которых имеет значение только тогда, когда он активно используется в качестве указателя, в то время как ссылка инкапсулирует не только набор битов, но также некоторые метаданные, которые хранят базовая структура сообщила о ее существовании. Если существует указатель на некоторый объект в памяти, и этот объект удален, но указатель не стирается, продолжение существования указателя не причинит никакого вреда до тех пор, пока не будет предпринята попытка доступа к памяти, на которую он указывает. Если не предпринять никаких попыток использовать указатель, ничто не позаботится о его существовании. В отличие от этого, фреймворки на основе ссылок, такие как .NET или JVM, требуют, чтобы система всегда могла идентифицировать каждую существующую ссылку на объект, и каждая существующая ссылка на объект всегда должна бытьnull или же определить объект надлежащего типа.

Обратите внимание, что каждая ссылка на объект фактически инкапсулирует два вида информации: (1) содержимое поля идентифицируемого объекта и (2) набор других ссылок на тот же объект. Хотя не существует какого-либо механизма, с помощью которого система могла бы быстро идентифицировать все ссылки, существующие на объект, набор других ссылок, существующих на объект, часто может быть самой важной вещью, инкапсулированной ссылкой (это особенно верно, когда объекты типа Objectиспользуются как такие вещи, как токены блокировки). Хотя система хранит несколько битов данных для каждого объекта для использования GetHashCode, объекты не имеют реальной идентичности, кроме набора существующих на них ссылок. Если Xсодержит единственную существующую ссылку на объект, заменяяXсо ссылкой на новый объект с тем же содержимым поля не будет иметь никакого идентифицируемого эффекта, кроме изменения битов, возвращаемых GetHashCode(), и даже этот эффект не гарантируется.

суперкар
источник
5

Указатели указывают на место в адресном пространстве памяти. Ссылки указывают на структуру данных. Все структуры данных все время перемещаются (ну, не так часто, но время от времени) сборщиком мусора (для сжатия пространства памяти). Кроме того, как вы сказали, структуры данных без ссылок через некоторое время будут собирать мусор.

Кроме того, указатели можно использовать только в небезопасном контексте.

Тамаш Чинеге
источник
5

Я считаю, что разработчикам важно понимать концепцию указателя, то есть понимать косвенное обращение. Это не означает, что они обязательно должны использовать указатели. Также важно понимать, что концепция ссылки отличается от концепции указателя , хотя и незначительно, но что реализация ссылки почти всегда является указателем.

Другими словами, переменная, содержащая ссылку, - это просто блок памяти размером с указатель, содержащий указатель на объект. Однако эту переменную нельзя использовать так же, как можно использовать переменную-указатель. В C # (и C, и C ++, ...) указатель может быть проиндексирован как массив, а ссылка - нет. В C # ссылка отслеживается сборщиком мусора, указатель - не может. В C ++ указатель можно переназначить, а ссылку нельзя. Синтаксически и семантически указатели и ссылки совершенно разные, но механически они одинаковы.

P Папа
источник
Дело в массиве звучит интересно, в том, что в основном вы можете указать указателю смещать ячейку памяти, как массив, в то время как вы не можете сделать это со ссылкой? Не могу представить, когда это будет полезно, но тем не менее интересно.
Ричард
Если p - это int * (указатель на int), то (p + 1) - это адрес, обозначенный p + 4 байтами (размер int). И p [1] то же самое, что * (p + 1) (то есть, он «разыменовывает» адрес на 4 байта после p). Напротив, со ссылкой на массив (в C #) оператор [] выполняет вызов функции.
P Daddy
5

Сначала я думаю, вам нужно определить «указатель» в вашей сематике. Вы имеете в виду указатель, который можно создать в небезопасном коде с фиксированным кодом ? Вы имеете в виду IntPtr, который вы получаете, возможно, от собственного вызова или Marshal.AllocHGlobal ? Вы имеете в виду GCHandle ? По сути, все это одно и то же - представление адреса памяти, где что-то хранится - будь то класс, число, структура, что угодно. И для справки, их, конечно, может быть в куче.

Указатель (все вышеперечисленные версии) - это фиксированный элемент. Сборщик мусора не знает, что находится по этому адресу, и, следовательно, не может управлять памятью или сроком службы объекта. Это означает, что вы теряете все преимущества системы со сборкой мусора. Вы должны вручную управлять объектной памятью, и у вас есть вероятность утечки.

С другой стороны, ссылка - это в значительной степени «управляемый указатель», о котором знает сборщик мусора. Это по-прежнему адрес объекта, но теперь сборщик мусора знает подробности о цели, поэтому он может перемещать ее, выполнять уплотнение, завершать, удалять и все другие приятные вещи, которые делает управляемая среда.

На самом деле основное различие заключается в том, как и почему вы их используете. В подавляющем большинстве случаев на управляемом языке вы собираетесь использовать ссылку на объект. Указатели становятся удобными для выполнения взаимодействия и редкой потребности в действительно быстрой работе.

Изменить: на самом деле вот хороший пример того, когда вы можете использовать «указатель» в управляемом коде - в данном случае это GCHandle, но то же самое можно было бы сделать с AllocHGlobal или с помощью фиксированного байтового массива или структуры. Я предпочитаю GCHandle, потому что мне он кажется более «.NET».

ctacke
источник
Небольшая оговорка, что, возможно, вам не следует говорить здесь «управляемый указатель» - даже с пугающими кавычками - потому что это нечто совершенно отличное от ссылки на объект в IL. Хотя в C ++ / CLI есть синтаксис для управляемых указателей, обычно они недоступны из C #. В IL они получаются с помощью инструкций (т.е.) ldloca и ldarga.
Glenn Slayden
5

Указатель может указывать на любой байт в адресном пространстве приложения. Ссылка жестко ограничена, контролируется и управляется средой .NET.

jdigital
источник
1

То, что делает указатели несколько сложными, заключается не в том, что они есть, а в том, что вы можете с ними делать. И когда у вас есть указатель на указатель на указатель. Вот когда действительно начинается самое интересное.

Роберт С. Барт
источник
1

Одно из самых больших преимуществ ссылок перед указателями - большая простота и удобочитаемость. Как всегда, когда вы что-то упрощаете, вы упрощаете его использование, но за счет гибкости и контроля вы получаете с помощью низкоуровневых вещей (как уже упоминали другие люди).

Указатели часто критикуют за «уродливость».

class* myClass = new class();

Теперь каждый раз, когда вы его используете, вам нужно сначала разыменовать его, либо

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

Несмотря на некоторую потерю удобочитаемости и добавление сложности, людям по-прежнему нужно было часто использовать указатели в качестве параметров, чтобы вы могли изменять фактический объект (вместо передачи по значению) и для повышения производительности без необходимости копировать огромные объекты.

Для меня именно поэтому ссылки были «рождены» в первую очередь, чтобы обеспечить те же преимущества, что и указатели, но без всего этого синтаксиса указателей. Теперь вы можете передать фактический объект (а не только его значение) И у вас есть более читаемый, нормальный способ взаимодействия с объектом.

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

Ссылки C ++ отличались от ссылок C # / Java тем, что, как только вы присвоили ему значение, которое было им, вы не могли повторно назначить его (и оно должно быть присвоено при объявлении). Это было то же самое, что и использование константного указателя (указателя, который нельзя перенаправить на другой объект).

Java и C # - это современные языки очень высокого уровня, которые убрали много беспорядка, накопившегося в C / C ++ за эти годы, и указатели определенно были одной из тех вещей, которые нужно было «очистить».

Что касается вашего комментария о знании указателей, которое делает вас более сильным программистом, в большинстве случаев это верно. Если вы знаете, «как» что-то работает, а не просто используете это, не зная, я бы сказал, что это часто может дать вам преимущество. Степень преимущества всегда будет разной. В конце концов, использование чего-либо, не зная, как это реализовано, - одна из многих прелестей ООП и интерфейсов.

Что в этом конкретном примере поможет вам со ссылками на указатели? Понимание того, что ссылка C # - это НЕ сам объект, а указывает на объект, - очень важная концепция.

№1: Вы НЕ передаете по значению. Для начала, когда вы используете указатель, вы знаете, что указатель содержит только адрес, вот и все. Сама переменная почти пуста, поэтому ее так приятно передавать в качестве аргументов. Помимо повышения производительности, вы работаете с реальным объектом, поэтому любые вносимые вами изменения не являются временными.

№2: Полиморфизм / Интерфейсы Когда у вас есть ссылка, которая является типом интерфейса и указывает на объект, вы можете вызывать только методы этого интерфейса, даже если объект может иметь гораздо больше возможностей. Объекты также могут по-разному реализовывать одни и те же методы.

Если вы хорошо понимаете эти концепции, я не думаю, что вы слишком много упускаете из-за того, что не использовали указатели. C ++ часто используется в качестве языка для изучения программирования, потому что иногда полезно испачкать руки. Кроме того, работа с низкоуровневыми аспектами позволяет вам оценить комфорт современного языка. Я начал с C ++, а теперь я программист на C #, и мне кажется, что работа с необработанными указателями помогла мне лучше понять, что происходит под капотом.

Я не думаю, что всем необходимо начинать с указателей, но важно то, что они понимают, почему используются ссылки вместо типов значений, и лучший способ понять это - взглянуть на его предка, указателя.

Despertar
источник
1
Лично я думаю, что C # был бы лучшим языком, если бы использовался в большинстве мест, где он .используется ->, но foo.bar(123)был синонимом вызова статического метода fooClass.bar(ref foo, 123). Это позволило бы такие вещи, как myString.Append("George"); [что изменит переменную myString ] и сделает более очевидной разницу в значениях между myStruct.field = 3;и myClassObject->field = 3;.
supercat