Как стать автором
Обновить

Комментарии 62

Более того, struct может вообще быть выделен не нами и даже не в куче/стеке. Например, можно где-то получить указатель на область памяти, привести его к (SomeStruct*), или ref SomeStruct (чтобы без unsafe) и дальше использовать как обычно. В графическом движке я так делаю с областью, проецируемой в видеопамять. Поэтому такое обобщение в предыдущей статье меня тоже сильно зацепило, но конечно не настолько, чтобы создавать статью-ответ и начинать холивар :)

Плюс JIT может тупо оптимизировать struct в значение регистра.

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

Модель памяти работает просто: инструкция lock представляет собой full fence, поэтому ни MemoryBarrier, ни volatilte здесь не нужны. Ну а само чтение значения переменной ссылочного типа атомарно.


Ну и небольшой ликбез по volatile: этот модификатор гарантирует очерёдность операций записи и очерёдность операций чтения. Чтение и запись при этом могут быть переставлены местами.

А можно ссылку где гарантируется full fence?
Возможно, мои данные уже устарели, но на текущий момент само MS и в MSDN и в сорцах corert использует lock + volatile…
А можно ссылку где гарантируется full fence?

Гуглится по запросу "c# implicit memory barriers"


но на текущий момент само MS и в MSDN и в сорцах corert использует lock + volatile

Покажите пример.


Я вот нашёл:
https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/Lazy.cs


Но там прям в комментариях написано, почему так делается:


_value = CreateViaDefaultConstructor();
_state = null; // volatile write, must occur after setting _value

Или вот:
https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/Threading/CancellationTokenSource.cs


Адская конструкция, зато без lock:


_state = NotifyingCompleteState; // _state is volatile
Volatile.Write(ref _executingCallbackId, 0);
Interlocked.MemoryBarrier(); // for safety, prevent reorderings crossing this point and seeing inconsistent state.

Ну а если используется lock + volatile, то скорее всего, это значит, что код писали криворукие индусы, и на истину в последней инстанции он явно не претендует.

Вы невнимательно прочитали комментарий. Смысл использования volatile — чтобы запись в _state произошла после записи в _value (кстати, в отличие от C++, volatile в C# предотвращает и аппаратный реордеринг). А вот блокировки там нет.

так что и lock и volatile оба присутствуют и оба используются

Неверно. Там не всё под локом сидит. Вот смотрите реализацию Value:


public T Value => _state == null ? _value : CreateValue();

Дальше смотрите код CreateValue и видите, что функции для части состояний вызываются уже без блокировки:


private T CreateValue()
{
    // we have to create a copy of state here, and use the copy exclusively from here on in
    // so as to ensure thread safety.
    LazyHelper? state = _state;
    if (state != null)
    {
        switch (state.State)
        {
            case LazyState.NoneViaConstructor:
                ViaConstructor(); // <-----
                break;
...
вы смотрите в неверный режим (там есть 3 режима по 3+4+3 стейта на каждый), нужно смотреть в режим ExecutionAndPublication***

Согласен, принимаю аргумент, использовать Lazy<> без поддержки thread safety как-то странно, когда речь идёт о доступе к нему из разных потоков. Но обратите теперь внимание, что здесь для описания состояния используется не одна переменная, а две: _state и _value. Соответственно, атомарно прочитать состояние уже невозможно.


Поэтому и используется volatile, чтобы гарантировать, что значение _state изменится до значения _value. В принципе, можно отказаться от volatile, тогда бы просто пришлось поменять код остальных функций, например:


private void ViaConstructor()
{
    _value = CreateViaDefaultConstructor();
    Thread.MemoryBarrier(); // <-- вот эту строчку надо добавить, если _state не является volatile
    _state = null;
}

Но если бы состояние описывалось одним объектом с атомарным доступом, то необходимости в volatile бы не было.

мне кажется, что мы с вами в итоге согласимся ))

моя мысль в статье была о том, что нам одного только голого lock не достаточно, и придётся добавить или явный барьер, или volatile.

Поскольку в классическом dcl у нас тоже две переменных, то там тоже это надо сделать. Но ещё лучше использовать готовый и протестированный код ферймворка, чем городить свой.
Поскольку в классическом dcl у нас тоже две переменных

Можно обойтись и одной для частного случая: когда используется только конструктор без параметров. Правда, в этом случае паттерн с созданием инстанса в статическом конструкторе оказывается ещё эффективнее.

Вот только для безопасной передачи значения между потоками требуется два барьера — один в пишущем потоке, второй в читающем.


А в паттерне DCL чтение происходит без инструкции lock, отсюда и необходимость использовать volatile.

Не согласен.


Ну прочитает второй поток null после того, как первый поток запишет туда какое-то значение. Ничего страшного, т.к. у нас DCL — второй раз он проверит это значение внутри lock, и справедливость восторжествует. Ну а последующие разы null он прочитать уже не сможет.


У меня есть ещё до кучи опасение, что при использовании volatile компилятор будет на каждый чих перечитывать это значение, хотя оно, будут однажды заданным, не поменяется уже никогда.

Проблема не в том, что он null может прочитать. Проблема в том, что он может прочитать объект до того как он будет сконструирован полностью.

В смысле, процессор поменяет местами инструкции и в переменную значение запишется раньше? На x86 и x64 такое невозможно. Хотя да, с приходом ARM об этом уже придётся задумываться.

Отличная статья!

Можно улучшать, но лучше в подобном стиле излагать другие стандарты и особенности.
Например про особое отношений компилятора к строкам и зачем появился StringBuilder.
Честно скажу, я написал эту статью почти сразу после появления оригинальной. Потом мне стало стыдно и я её стёр, не запостив. Но сегодня решил: не пропадать же такой простыне текста ))

Про строки: у меня просто пропал запал и мне сейчас не интересно это писать. Но вы можете сделать это сами. Даёшь больше качественного контента!
Я автор того самого сборника заблуждений. Т.к. данная статья ссылается на мою шпаргалку, вынужден дать комментарий.

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

Ниже более развернуто

Ссылочные и значимые типы (value vs reference types)
действительно, значимые типы, являющиеся локальными переменными в текущей реализации скорее всего окажутся на стэке.
Для классов (ссылочных типов) на текущий момент действительно верно, что во всех простых случаях они окажутся размещёнными в куче.

Если верно — значит не заблуждение, в «текущей реализации» для подавляющего большинства случаев.

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

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

Передача по значению / указателю
Про ref и out в шпаргалке есть.
А говорится так потому, что при передаче refernce type в методе получится переменная указывающая на тот же самый объект.

string — особенный тип
Про какие особенности речь?
1. Это неизменяемый (immutable)
2. Можно сравнивать с помощью оператора==

Да про эти особенности. Заголовок можно поменять «string — особенный тип» на «string». Сути дела это не меняет, на собеседованиях спрашивают именно про эти 2 особенности.

const vs readonly
Значение константы фиксируется на момент компиляции. А статических readonly-полей (которые часто используют как замену) — на этапе выполнений.
соберите тестовый проект, и откройте его в декомпиляторе. Вы увидите, что вместо констант встанут их значения

В шпаргалке ровно это написано, приведен тоже самый пример с компиляцией

Finalizer (destructor) ~
Тут чисто термилогический вопрос, не нравится вам что в C# finalizer называют ещё и destructor. Ну это и Microsoft так называет (сами ссылку дали)

Заблуждение (3): вызывается только автоматически средой .Net, нельзя вызвать самостоятельно

Хорошо, добавили что через reflection можно. Утверждение, что приватные методы не для того что-бы из вызывали снаружи, ни как не является заблуждением.
Кстати что говорит «Закон — ECMA 334» по поводу использования reflection.

Еще раз спасибо автору за критику и дополнения.
> для подавляющего большинства случаев

в подавляющем большинстве случаев структы лежат в куче, можете открыть dotMemory и убедиться

> дополнительные нюансы

это не нюансы, если вы пишете не-тривиальный софт, то бОльшую часть времени он использует данные, а не аллоцирует их

> Ну это и Microsoft так называет

прочитайте, что именно Microsoft написали в стандарте. И что им очень за это стыдно
На мои комментарии там (в шпаргалке) вы не ответили, давайте тут продолжим :)

Передача по значению / указателю

Вы удивитесь, но все параметры, кроме out и ref, передаются по значению, т.е. копируются в стек. И именно тут ваша шпаргалка вводит неофитов в заблуждение.
Вот только для reference типа копируется ссылка, а value тип копируется целиком.
И из-за непонимания этих фактов получаются забавные баги:
  • Многие пытаются в методе изменить объект reference-типа просто присвоим переменной в методе новое значение.
  • и классика жанра — поменять поле value-типа и удивляться тому, что после вызова метода всё осталось как есть.

string — особенный тип

вот только вы упорно говорите, что string ведёт себя как value-тип, что совершенно не верно. Ну и про конкатенацию не то пишете. Тип и правда интересный и полезно знать его особенности, но про них вы не пишете.
классика жанра — поменять поле value-типа и удивляться тому, что после вызова метода всё осталось как есть.

Да, вот забавный сценарий:


struct Q
{
    public int Value;

    public void IncrementValue(int value)
    {
        Value += value;
    }
}

var arr = new[] { 1 };
var arr2 = new[] { new Q { Value = 1 } };
var list = new List<int> { 1 };
var list2 = new List<Q> { new Q { Value = 1 } };

arr[0] += 2;
arr2[0].IncrementValue(2);
list[0] += 2; 
list2[0].IncrementValue(2);

Угадайте, в каких случаях значение элемента коллекции поменяется.

Угадайте, в каких случаях значение элемента коллекции поменяется.

Я вообще джавой занимаюсь, но очень хочу поугадывать.


var arr = new[] { 1 };
arr[0] += 2;

Создаётся массив, элементы которого содержат непосредственно инты. Значение элемента поменяется.


var arr2 = new[] { new Q { Value = 1 } };
arr2[0].IncrementValue(2);

Создаётся массив с элементами типа Q. В статье написано, что переменных типа struct хранят непосредственно данные.


Поэтому я думаю, что содержимое элемента поменяется.


var list = new List<int> { 1 };
list[0] += 2; 

Лист маленьких интов. Первое, что вручают джава разработчикам, попавшим в Валгаллу. Если тут не меняется значение элемента — существование C# не имеет смысла. Мы заранее знаем, что C# штука глубоко осмысленная, следовательно значение элемента коллекции меняется.


var list2 = new List<Q> { new Q { Value = 1 } };
list2[0].IncrementValue(2);

Лист объектов типа Q. У одного из элементов вызвывается метод, что не может поменять значение элемента в том случае, если память не выделятеся прямо в листе.


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


Update: Перечитал статью и понял, что ref относится к параметрам, а не к переменным. Теперь что будет в последнем примере мне неясно. Но всё равно хотелось бы, чтобы менялось значение элемента.

Тут дело не в параметрах, но близко. В последней строке при получении элемента из листа возвращается копия всей структуры, а сохранения нет (в отличии от предыдущей строки), поэтому значение в листе не изменится.
Тут дело не в параметрах, но близко.

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


Если бы метод возвращал ссылку, для чего тоже надо использовать ключевое слово ref, то тоже получилось бы изменить именно элемент. Но принято осознанное решение вернуть именно копию.

Можно его и на переменную поставить, и даже на возвращаемое значение. Но оно в классе List не ставится, вот в чём проблема.

Можно его и на переменную поставить, и даже на возвращаемое значение.

Точно, можно. Надо было читать документацию.


Но оно в классе List не ставится, вот в чём проблема.

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

Мне кажется, что так сделано по двум причинам:


  1. Legacy. Изначально никаких ref return не было, и привилегией возврата значения по ссылке при индексации обладали исключительно массивы. И если сейчас, с появлением ref return, изменить логику работы коллекций, то это поломает существующий код.


  2. При добавлении элементов в List не гарантируется сохранение ссылок на элементы. То есть следующий код:


    ref var elem = ref list[0];
    list.Add(...);
    elem.IncrementValue();

    будет некорректным.


Смотрим какой IL генерит каждая строчка (в пересказе на русский):
arr[0] += 2;
Грузит в стэк адрес элемента массива, грузит в стэк инт по этому адресу, увеличивает на 2, и сохраняет обратно по адресу

arr2[0].IncrementValue(2);
Грузит в стэк адрес элемента массива, и вызывает его метод

list[0] += 2; 
Вызывает геттер, увеличивает полученное значение на 2, вызывает сеттер.

list2[0].IncrementValue(2);
Вызывает геттер (который возвращает, естественно, копию), вызывает метод этой копии. Более не делает ничего, соответственно копия просто отправляется в страну вечной охоты, исходный лист остаётся неизменным.

Итого, первые три строчки меняют значение, четвёртая — нет.

Finalizer (destructor) ~
Тут чисто термилогический вопрос, не нравится вам что в C# finalizer называют ещё и destructor. Ну это и Microsoft так называет (сами ссылку дали)

Тоже так считал до того момента, пока мне не подсказали посмотреть в стандарт C++/CLI. Вот что дает ECMA-372, 1st edition, December 2005:


  1. Лучше расписано в чем заключается различие (8.8.8 Destructors and finalizers).
  2. Дестркутор вызывает метод Dispose (34.7.13.2 Destructors).

К тому же, стандарт CLI говорит о том, что финализатор может вызваться не один раз (I.8.9.6.7 Finalizers — ECMA-335, 6th edition, June 2012).


После этого, думаю, не стоит смешивать эти два понятия :)

string — особенный тип

Все верно. Это тип, экземпляры которого имеют произвольный размер. Самому такой не сделать. Как массив.

Эта особенность принимается, действительно!

Но если учесть что реализация System.String содержит в себе кучу unsafe-магии, то мы можем сделать свой класс воспользовавшись той же магией.

Не, там магия на уровне CLR сидит:
https://mattwarren.org/2016/05/31/Strings-and-the-CLR-a-Special-Relationship/
Придётся слишком глубоко вносить изменения.


Если же рассматривать реализацию через пользовательский код, тогда можно сделать враппер над массивом, который будет трактовать элементы массива как длину или символы. Единственный нюанс: этот враппер будет структурой, а не классом. Ну и производительность похуже нативной реализации будет.

От CLR-магии нам нужен только резиновый аллокатор, сигнатура которого известна, так что глубоко вносить изменения не придётся ))

Либо можно на него вообще забить и юзать AllocHGlobal, но я не уверен, что на это скажет GC увидев ссылку в странное место…

GC занимается перемещением объектов в памяти. Он знает про строки и про массивы. Произвольных объектов произвольного размера в нем не описано, он не сможет их обработать и сломает при переносе.

А как он может сломать объект произвольного размера, который не содержит ссылок?

Когда GC захочет переместить объект в другое место, сколько байт он должен скопировать?

А об этом он узнает, посмотрев в кучу.

И что он в этой куче увидит? Или вы думаете что GC пользуется c/c++ функциями malloc/realloc/free?


Размеры объектов в куче не хранятся. В куче хранится ссылка на тип (точнее на "таблицу методов") объекта, в котором есть его размер.


Вот тут в заголовке класса Object есть целый параграф про его размеры
https://github.com/dotnet/runtime/blob/master/src/coreclr/vm/object.h

Спасибо, теперь стало действительно понятнее (сам не смог найти, где
смотреть).


Отдельно доставило (methodtable):


    BOOL IsStringOrArray() const
    {
        LIMITED_METHOD_DAC_CONTRACT;
        return HasComponentSize();
    }

    BOOL IsString()
    {
        LIMITED_METHOD_DAC_CONTRACT;
        return HasComponentSize() && !IsArray() && RawGetComponentSize() == 2;
    }

То есть в CLR прям жёстко прописано, что хвостатых типов может быть только два.

А если воспринимать строку как массив чаров, то всё в порядке =)
Более того, со строкой во многом можно работать как с массивом, это фича. Ну и, реализация IEnumerable дает немножко экстеншенов для работы с перечислениями.

Вот только массив тоже особенный тип и самому такой также не сделать :)

Заблуждение:

if (Evt != null)
    Evt("hello");


Закон: 15.8.2 Field-like events
EventHandler handler = Click;
if (handler != null)
    handler(this, e);


Товарищи, может кто нибудь прояснить в чем смысл тут?

Когда-то давно кто-то подумал что все наверняка будут писать многопоточные программы, в которых будут вызывать делегаты и истерично на них подписываться-отписываться безо всякой синхронизации. Вот оттуда эта конструкция и пошла. Типа, между проверкой на null и вызовом делегата другой поток мог успеть отписаться и тогда код свалится по NRE. При этом почему-то умалчивают о том что такой "исправленный" код вовсе и не факт что правильный. В нём делегат вызовут уже после того как произошла отписка. Но совсем не факт что программа вообще готова к такому.

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

Вы не поверите, но я это видел в жизни на 2х разных проектах. Чтобы попасть между строками, не обязательно истерично подписываться и отписываться. Оба раза это была нетривиальная зависимость между разными событиями, и внутри хендлера одного события отписывались от другого.

> В нём делегат вызовут уже после того как произошла отписка.
да, в MSDN так и пишут, что описка не гарантирована
Чтобы попасть между строками, не обязательно истерично подписываться и отписываться.

Чтобы попасть между вот именно теми двумя строками, которые приведены в «заблуждении про делегаты», нужно не меньше двух потоков. И тогда возникает вопрос — а при чём здесь собсна делегаты? То же самое справедливо для любого мутабельного nullable поля. Между проверкой на null и использованием значения может ворваться второй поток и записать туда null.
Надеюсь, разницу, объяснять не надо.

Конечно не надо. Разница следующая. Вот это хороший код:


if (Evt != null)
    Evt("hello");

А вот это код попахивающий и подозрительный:


EventHandler handler = Click;
if (handler != null)
    handler(this, e);

В первом случае код, видимо, не предендует на работоспособность в мультипотоке и всё хорошо. Во втором, видимо, предполагается, что код должен работать в мультипотоке, но скорее всего он не будет правильно работать, потому что просто бездумно скопировать хэндлер для этого обычно недостаточно, и вообще эвенты и мультитрединг — не лучшее соседство.

Кстати, про out- параметры.
Наткнулся недавно на момент, показавшийся интересным.


Оказывается, не для всех out- параметров обязательна инициализация до возврата метода.


Может будет интересно — написал про это небольшую статью. Погружение в код Roslyn прилагается. :)

Да, статья отличная, уже оценил ) Спасибо!

Вам спасибо за оценку. :)

Неплохая статья, правда немного догматичная. Хотя всопнмить мою первую статью на хабр… Там от количества самомнения её в итоге скрыть пришлось и карму обнулять :)


Ссылочные и значимые типы (value vs reference types)

Cамый простой контрпример:


var x = (object)12;
Span<int> y = stackalloc int[12];

инт это структура, но забокшенная она находится в куче, и забоксится может по 547 разных причин. Массивы традиционно являются ссылочными типами, но застакаллоченные лежат на стеке. Можно немного придраться что это массивы не наследующие привычный T[], но позвольте, все не SZArray массивы такие по сути.


Подробнее хорошо написано у Липперта здесь, здесь и здесь (увы, мсдн потер старые статьи, но слава богу у нас есть web.archive). Как можно видеть, Липперта эти сравнения бесили ещё в 2009, так что ничто не ново под луной


stack vs heap

Ну тут говорить нечего, стек быстрее потому что он бесплатно очищается при выходе из функции, а в куче надо сначала выделить память (сравнительно быстро) а потом ещё и собрать (очень небыстро). То что стек не резиновый — вроде прописная истина, это все понимают.


Заблуждение: значимые типы (структуры) передаются по значению а ссылочные (классы) — по ссылке.

На разговорном уровне это верно. На менее разговорном — структура состоит из себя самой, а класс — из указателя на данные. Копирование структуры приводит к копии всех данных, копирование указателя — к копии указателя (на те же самые данные). Если представить, как байтики в памяти размещаются, поведение становится совершенно предсказуемым. Тот же вопрос иногда в JS мире задают, тип почему во тут поменялось значение а вот тут — нет. Хотя там никаких структур и нет.


const vs readonly

Забавный факт: в MSIL есть опкоды для загрузки статик полей, а для загрузки констант — нет. То есть в IL вообще не существует никаких констант. Это было для меня в свое время прям раскрывающим глаза открытием.


ref и out

Just don't. Майкрософт наконец сделали юзабельные таплы — используйте их всегда, когда нужно вернуть пару значений, для всяких TryParse(...) используйте нулляблы, и будет всем счастье.


События, делегаты

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


Заблуждение (1): ~Foo() это «деструктор» класса Foo

Достаточно считать что деструктор в сишарпе это не то же что деструктор в C++ и проблем с терминологией не будет — никаких других деструкторов в языке нет и перепутать не с чем.


Заблуждение: не забудьте про потокобезопасность и lock

Ваш способ самый правильный для синглтона без зависимостей, Если есть зависимости — то лези это хороший выбор, а ещё лучше — просто взять диай и написать AddSingleton<IFoo, Foo>()

Как выше верно написали, раньше код мог иногда с наллрефом упасть, а теперь может вызываться код который отписался.

Он и раньше мог вызвать отписанный делегат, так что вариант с проверкой на null безусловно лучше варианта без проверки.


"Ошибку" с вызовом отписанного делегата можно устранить только если делать все три операции (add/remove и invoke) под локом. Но делать так смысла немного.

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


А во втором варианте имеем молчаливое потенциально некорректное поведение

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

Ну я как раз про это. Если кто-то наменял между двумя этими точками содержимое поля — это может сигнализировать об ошибке. Собственно, это не раз ещё Акиньшин лет 5 назад рассказывал на нескольких докладах.


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

Спасибо за подробный ответ и за ссылки на Эрика Липперта (добавил в статью)!

С некоторыми пунктами позволю себе не согласиться:

> То есть в IL вообще не существует никаких констант

А как же семейство инструкций ldc.*? а декларации literal?
Возможно, вы имели в виду то, что загрузка констант использует числа (результат вычисления выражений) напрямую, а не ссылаясь на именованный литерал, в отличие от readonly и вообще всех других полей.

>> Заблуждение: значимые типы (структуры) передаются по значению…
> На разговорном уровне это верно… Тот же вопрос иногда в JS мире задают…

В JS всё всегда передаётся по значению (как и в Java), просто там нет двух ортогональных понятий (тип объекта и способ передачи), поэтому к такому относятся более снисходительно. Хотя у меня ни один кандидат, давший такой ответ, собеседование не прошёл.
В C#, поскольку есть две независимых категории, это отличие очень важно. Вы можете передать 4-мя способами: значимый тип по значению, значимый по ссылке, ссылочный по значению, ссылочный по ссылке (указатель на указатель, если сравнивать с плюсами).

>> ref и out
> Just don't.

С тем, что не стоит это использовать без крайней необходимости (напр. PInvoke), соглашусь. Но знать и понимать это надо.

> ведь мсдн разрешает вызывать отписавшиеся делегаты — мне кажется немного самоуверенным.

Ну так он же разрешает. Значит лучше вызвать протухший делегат, чем упасть с эксепшеном.

> Достаточно считать что деструктор в сишарпе это не то же что деструктор в C++ и проблем с терминологией не будет.

Если вы работаете на проекте с плюсовиками, то чтобы их не путать, лучше называть так, как это написано в стандарте. И как это стараются переписать в MSDN (можно сравнить старые и свежие версии статей, майкрософт постепенно подчищает за собой это недоразумение).
А как же семейство инструкций ldc.*? а декларации literal?

Про литералы/константы понятно. Я про то что нет никакого аналога Ldsflda чтобы достать скажем int.MaxValue.


В JS всё всегда передаётся по значению (как и в Java), просто там нет двух ортогональных понятий (тип объекта и способ передачи), поэтому к такому относятся более снисходительно. Хотя у меня ни один кандидат, давший такой ответ, собеседование не прошёл.

Там была задачка на завуалированное a => a = {x: 10} vs a => a.x = 10, которое очевидным образом в одном случае работало, а в другом — нет. Что-то вокруг const/let/var, уже не помню детали к сожалению. Но я помню что я жсерам рассказывал про то как в памяти ссылки устроены и почему эти два действия дают разный результат.


С тем, что не стоит это использовать без крайней необходимости (напр. PInvoke), соглашусь. Но знать и понимать это надо.

Ну, лишние знания не вредят, но вопрос приоритетов. 99% разрабов например кейворд unsafe не пригодился ни разу в жизни и они может вообще не знают, что это и зачем нужно. Нужно ли им его знать и понимать? В целом, скорее нет, чем да. В общем, как я уже сказал, знания лишними не бывают, но это я бы отнёс к очень низкоприоритетным.


Ну так он же разрешает. Значит лучше вызвать протухший делегат, чем упасть с эксепшеном.

Не факт. Жесткое падение зачастую лучше молчаливой некорректной работы. Даже если дока такое разрешает. А может и лучше, как я уже сказал, это вопрос конкретного приложения как мне кажется, и если вы решили что вам лучше вызвать отписавшийся делегат — ну, возможно в вашем случае так и нужно. Минусы выше без комментов как раз показывает, что любое сомнение в этой мантре — грех. Не надо сомневаться — надо писать вот так как в книжке написано :)

> Жесткое падение зачастую лучше молчаливой некорректной работы

так и в некорректном варианте возможен молчаливый вызов уже отписанного делегата. Падение эксепшена там ни разу не гарантированно. Да, я видел этот эффект в жизни.

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

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