Комментарии 63
Дело в том, что модель памяти работает не совсем так, как кажется на первый взгляд
Модель памяти работает просто: инструкция lock
представляет собой full fence, поэтому ни MemoryBarrier
, ни volatilte
здесь не нужны. Ну а само чтение значения переменной ссылочного типа атомарно.
Ну и небольшой ликбез по volatile
: этот модификатор гарантирует очерёдность операций записи и очерёдность операций чтения. Чтение и запись при этом могут быть переставлены местами.
Возможно, мои данные уже устарели, но на текущий момент само 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
Адская конструкция, зато без 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, то скорее всего, это значит, что код писали криворукие индусы, и на истину в последней инстанции он явно не претендует.
так ведь тут _state как раз и объявлен volatile:
private volatile LazyHelper? _state;
Вы невнимательно прочитали комментарий. Смысл использования volatile
— чтобы запись в _state
произошла после записи в _value
(кстати, в отличие от C++, volatile
в C# предотвращает и аппаратный реордеринг). А вот блокировки там нет.
вот внутри него используется поле _state: github.com/dotnet/corert/blob/9d8bd29a71f68941aaa2c00e9c9a0eed86cafa10/src/System.Private.CoreLib/shared/System/Lazy.cs#L309
а вот декларация volatile: github.com/dotnet/corert/blob/9d8bd29a71f68941aaa2c00e9c9a0eed86cafa10/src/System.Private.CoreLib/shared/System/Lazy.cs#L191
так что и lock и volatile оба присутствуют и оба используются
так что и 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;
...
Согласен, принимаю аргумент, использовать 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 чтение происходит без инструкции lock, отсюда и необходимость использовать volatile.
Не согласен.
Ну прочитает второй поток null
после того, как первый поток запишет туда какое-то значение. Ничего страшного, т.к. у нас DCL — второй раз он проверит это значение внутри lock
, и справедливость восторжествует. Ну а последующие разы null
он прочитать уже не сможет.
У меня есть ещё до кучи опасение, что при использовании volatile
компилятор будет на каждый чих перечитывать это значение, хотя оно, будут однажды заданным, не поменяется уже никогда.
Проблема не в том, что он null может прочитать. Проблема в том, что он может прочитать объект до того как он будет сконструирован полностью.
Можно улучшать, но лучше в подобном стиле излагать другие стандарты и особенности.
Например про особое отношений компилятора к строкам и зачем появился 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 не ставится, вот в чём проблема.
Об этом я и говорю, когда пишу, что было принято осознанное решение вернуть копию.
Мне кажется, что так сделано по двум причинам:
Legacy. Изначально никаких ref return не было, и привилегией возврата значения по ссылке при индексации обладали исключительно массивы. И если сейчас, с появлением ref return, изменить логику работы коллекций, то это поломает существующий код.
При добавлении элементов в List не гарантируется сохранение ссылок на элементы. То есть следующий код:
ref var elem = ref list[0]; list.Add(...); elem.IncrementValue();
будет некорректным.
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:
- Лучше расписано в чем заключается различие (8.8.8 Destructors and finalizers).
- Дестркутор вызывает метод 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/
Придётся слишком глубоко вносить изменения.
Если же рассматривать реализацию через пользовательский код, тогда можно сделать враппер над массивом, который будет трактовать элементы массива как длину или символы. Единственный нюанс: этот враппер будет структурой, а не классом. Ну и производительность похуже нативной реализации будет.
Либо можно на него вообще забить и юзать 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. При этом почему-то умалчивают о том что такой "исправленный" код вовсе и не факт что правильный. В нём делегат вызовут уже после того как произошла отписка. Но совсем не факт что программа вообще готова к такому.
Вы не поверите, но я это видел в жизни на 2х разных проектах. Чтобы попасть между строками, не обязательно истерично подписываться и отписываться. Оба раза это была нетривиальная зависимость между разными событиями, и внутри хендлера одного события отписывались от другого.
> В нём делегат вызовут уже после того как произошла отписка.
да, в MSDN так и пишут, что описка не гарантирована
Чтобы попасть между строками, не обязательно истерично подписываться и отписываться.
Чтобы попасть между вот именно теми двумя строками, которые приведены в «заблуждении про делегаты», нужно не меньше двух потоков. И тогда возникает вопрос — а при чём здесь собсна делегаты? То же самое справедливо для любого мутабельного nullable поля. Между проверкой на null и использованием значения может ворваться второй поток и записать туда null.
Кстати, про 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
не пригодился ни разу в жизни и они может вообще не знают, что это и зачем нужно. Нужно ли им его знать и понимать? В целом, скорее нет, чем да. В общем, как я уже сказал, знания лишними не бывают, но это я бы отнёс к очень низкоприоритетным.
Ну так он же разрешает. Значит лучше вызвать протухший делегат, чем упасть с эксепшеном.
Не факт. Жесткое падение зачастую лучше молчаливой некорректной работы. Даже если дока такое разрешает. А может и лучше, как я уже сказал, это вопрос конкретного приложения как мне кажется, и если вы решили что вам лучше вызвать отписавшийся делегат — ну, возможно в вашем случае так и нужно. Минусы выше без комментов как раз показывает, что любое сомнение в этой мантре — грех. Не надо сомневаться — надо писать вот так как в книжке написано :)
Старые статьи на MSDN не потёрли, а хорошенько перепрятали:
del
Неужели люди, готовящиеся к собесу по дотнету, не знают этих вещей. Это достаточно базовые вещи. Но судя по всему оно может много кому помочь...
Собеседования бывают разными — где-то тебя 5 минут гоняют по linq'у и спросят пару ключевых слов из sql, а где-то просят накидать сортировку пузырьком и какой-то запрос на N таблиц с джойнами, а где-то конечно спросят про аллоцирование памяти, типы данных и что-то ещё, но мне такие собесы не попадались.
Уровни программистов-читателей — разные; источники информации, откуда начинающие черпают информацию — тоже разные; а ещё разнится степень запоминания из этих источников... можно долго и много писать о том, почему такие статьи (и комментарии к ней) могут пригодиться.
Лично для меня частота применения структур на одном из проектов, где я работал, стремится к нулю, а на другом — равна нулю =) А причина в том, что узкие места на этих проектах сосредоточены в других аспектах относительно преимуществ структур. Скажем, не в процессоре/памяти при работе с poco-данными, а например с БД/сетью, поэтому лично я не углублялся в структуры, успешно работая несколько лет. А всё то, что знал с начала начал — кроме "класс в куче, а структура значимый тип и в стеке" — забыл.
Статья и комменты оказались очень интересными.
Популярные заблуждения о C#