Невероятные приключения в коде

Перевод блога Эрика Липперта

  • В чём разница между операторами «as» и «приведения»?

    Большинство людей скажут, чт о разница между «(Alpha)bravo» и «bravo as Alpha» в том, что первое бросает исключение при неуспехе преобразования, а последнее возвращает null. Хоть это и правильно, и это самая очевидная разница, дело не только в этом. Здесь есть ловушки, которых нужно остерегаться.

    Во-первых, поскольку результатом оператора «as» может быть null, целевым может быть только такой тип, который допускает значение null: либо ссылочный тип, либо Nullable тип-значение. Нельзя сделать «as int», это не имеет никакого смысла. Если аргумент не int, то каким должно быть возвращаемое значение? Выражение «as» всегда возвращает указанный тип, так что он должен быть типом, допускающим null.

    Во-вторых, оператор приведения, как я уже обсуждал – странный зверёк. Он имеет два противоречивых смысла: «проверь, что этот объект действительно этого типа, брось исключение, если не так», и «этот объект не этого типа; найди мне эквивалентное значение, которое принадлежит этому типу». Второе значение оператора приведения не поддерживается оператором «as». Если вы написали

    short s = (short)123;
    int? i = s as int?;

    то вам не повезло. Оператор «as» не станет делать изменяющие представление преобразования из short в nullable int, как стал бы оператор приведения. Аналогично, если у вас есть класс Alpha и несвязанный с ним класс Bravo, с пользовательским оператором преобразования из Bravo в Alpha, то «(Alpha)bravo» применит это преобразование, а «bravo as Alpha» – нет. Оператор «as» учитывает только ссылочные преобразования, и упаковку/распаковку типов-значений.

    И, в-последних, сценарии использования этих двух операторов, конечно же, имеют поверхностное сходство, но весьма различны семантически. Приведение сообщает читателю «я уверен, что это преобразование законно, и я готов получить исключение при исполнении, если я ошибся» Оператор «as» сообщает «Я не знаю, можно ли провести это преобразование; мы дадим ему шанс, и посмотрим, что получилось».

  • Почему нет свойств расширения?

    Меня часто спрашивают «парни, вы добавили методы расширения в C# 3, так почему бы не добавить ещё и свойства расширения?»

    Хороший вопрос.

    Давайте, сначала я чуть поговорю о C# 3.0. Явно главной новостью в C# 3 был LINQ. В некотором смысле, у нас было всего три новых возможности в C# 3:

    • всё необходимое для LINQ – неявно типизированные переменные, анонимные типы, лямбда-выражения, методы расширения, инициализаторы объектов и коллекций, конструкторы запросов, деревья выражений, улучшенный вывод типов
    • частичные методы
    • автоматически реализуемые свойства

    Последние два были крошечными по сравнению с пунктами из первой пачки. С точки зрения проектирования, синтаксис и семантика обеих возможностей прямолинейны. С точки зрения реализации, у нас уже были механизмы для устранения вызовов; частичные методы и условные методы – почти одно и то же за кулисами. Авто-свойства также были весьма прямолинейны в анализе и генерации кода. Нагрузка по тестировнию тоже была для них не особенно большой.

    Команда C# была «длинным шестом» в 2008 релизе Visual Studio и .NET Framework. Под этим я имею в виду то, что если бы вы взяли количество времени, нужно для выполнения работы, на которую подписалась каждая команда, с учётом численности персонала, всё такое, и сделали шесты пропорциональной этому длины для каждой команды в Developer Division, то шест C# оказался бы самым длинным. Что означает, что у любой другой команды в DevDiv был запас в расписании, но если бы мы отстали на день от своего расписания, то новая студия и CLR тоже были бы выпущены на день позже. Если бы любая другая команда отстала на день, ну, до тех пор, пока это не делало их шест длиннее нашего, у них всё ещё было бы всё в порядке. Внутри нашей команды, при разделе работы между разными её участниками, «длинным шестом» были задачи привязывания лямбд и вывода типов методов, отданные мне. Так что, в некотором смысле, каждый день моего опоздания одначал запаздывание выхода продукта на столько же дней. (Никакого давления!)

    К счастью, у нас тут превосходная команда, мы все очень хорошо помогали друг другу в течение этого релиза, подхватывая задачи друг друга при необходимости, и выполнили качественный релиз за долгое время. Моя мысль – в том, что если что-то не было либо необходимым для LINQ, либо маленьким, ортогональным, и легковыбрасываемым в случае необходимости (как остальные две), это выбрасывали немедленно. Ни в коем случае мы не собирались рисковать задержать выход всего продукта для какой-то штуки, которая была бы сразу и сложной и не необходимой. Конечно же, сразу было очевидно, что естественным спутником методов расширения являются свойства расширения. Менее очевидно, по какой-то причине, что события расширения, операторы расширения, конструкторы расширения (также известные, как «паттерн «фабрика»»), и так далее, тоже являются естественными спутниками. Но мы даже не рассматривали вопрос проектирования свойств расширения в C# 3; мы знали, что без них можно обойтись, и они добавят риск в и итак уже рискованное расписание, без привлекательного выигрыша.

    Ну, теперь мы дошли до C# 4.

    Как я люблю напоминать, ответ на все вопросы вида «почему в продукте X нет возможности Y?» один. Этопотому, что для того, чтобы в продукте была некоторая возможность, эта возможность должна быть

    • в первую очередь продумана
    • нужна
    • спроектирована
    • специфицирована
    • реализована
    • протестирована
    • задокументирована
    • доставлена потребителям

    Мы должны пройти каждый из этих пунктов, иначе – никаких новшеств.

    Когда мы начали работать над C# 4, мы составили список всех запросов, о которых мы только слышали. В нём были сотни возможностей. Как я описывал в прошлом году, мы рассортировали тот список по корзинкам «стоит сделать / неплохо бы / плохая идея». Свойства расширения были в корзинке «стоит сделать». Затем мы посмотрели на доступный нам бюджет – который измерялся не столько в долларах, сколько в доступных проектировщиках, разработчиках, тестерах, писателях, и менеджерах, умноженных на доступное время – и определили, что у нас хватит ресурсов на реализацию примерно половины вещей в корзинке «стоит сделать». Так что мы выбросили половину этого. Свойства расширения пережили это урезание. Затем мы спроектировали эту возможность. У нас были многочасовые споры о предполагаемом синтаксисе деклараций, способах «прямого» вызова методов чтения и записи как статических методов, и так далее. Мы придумали и согласовали приемлемый синтаксис, спроектировали семантику, написали черновик спецификации, и начали писать код и планы тестирования.

    К моменту, когда код дошёл до приличного состояния – еще не до конца протестирован, но работает и соответствует спецификации – мы стали устраивать собрания с командой WPF. Разработчики WPF были предполагаемыми основными потребителями свойств расширения. В WPF уже есть механизм, похожий на свойства расширения; было бы неплохо унифицировать этот механизм с нашим. К сожалению, рассмотрев хорошенько их реальные сценарии, мы прили к разочаровывающему выводу о том, что мы спроектировали не то; эта возможность на самом деле не решала их проблемы. Это, надо признать, провал процесса проектирования. Мы должны были поинтересоваться мнением основного потребителя намного раньше. Сделай мы это – и они могли бы либо повлиять на дизайн и получить что-то, пригодное для них, или бы мы выбросили эту возможность из плана без затрат на написание спецификации, кода, и планов тестирования.

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

    Так что, к сожалению, никаких свойств расширения в C# 4. Возможно, в гипотетической будущей версии C#.

  • Почему Char неявно конвертируется в ushort, но не наоборот?

    Ещё один хороший вопрос со StackOverflow. Почему есть неявное преобразование из char в ushort, но только явное из ushort в char? Почему дизайнеры языка верят, что эти асимметричные правила имело смысл добавлять в язык?

    Ну, во-первых, очевидные вещи, которые бы не дали любому из преобразований быть неявным, тут неприменимы. Char реализован как беззнаковое 16-битное целое, которое представляет символ в кодировке UTF-16, так что его можно преобразовывать в ushort и обратно без потери точности, или, в данном случае, без изменения представления. Среда просто переходит от трактовки этого набора бит как символа к трактовке того же набора бит как ushort, или обратно.

    Так что можно позволить оба неявных преобразования. Но, только то, что что-то возможно не означает, что это хорошая идея. Явно дизайнеры языка думали, что неявное преобразование char в ushort - хорошая идея, но неявное преобразование из ushort в char – нет. (А, поскольку char в ushort - хорошая идея, то выглядит разумным, что char во что-угодно-во-что-преобразуется ushort тоже, так что char в int тоже можно.)

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

    Преобразовение из ushort в char упоминается в заметках от 14 апреля 1999, где поднимается вопрос, стоит ли разрешать преобразование из байта в char. В исходной пред-релизной версии C#, это было разрешено. Я слегка подредактировал заметки, чтобы сделать их ясными без понимания пред-релизных кодовых названий Майкрософт эпохи 1999. Я также добавил выделение в важных местах:

    [Комитет по проектированию языка] выбрал предоставление неявной конверсии из байтов в символы, поскольку область значений первых полностью покрывается вторыми. Тем не менее, прямо сейчас [авторы библиотеки среды исполнения] предоставляют только методы Write, которые принимают charы и intы, что означает, что байты печатаются как символы, поскольку этот метод оказывается наилучшим. Мы можем решить это либо предоставлением дополнительных методов Write, либо устранением неявного преобразования.

    Есть аргумент в пользу того, что последнее является правильным действием. В конце концов, байты на самом деле не символы. Да, тут может иметь место полезное отображение байтов в символы, но, фундаментально, 23 не обозначает то же самое, что и символ с ASCII кодом 23, в том же смысле, как байт 23 обозначает то же самое, что и длинное целое 23. Просить [разработчиков библиотеки] предоставить этот дополнительный метод просто из-за особенностей нашей системы типов кажется достаточно слабым.

    Заметки затем заканчиваются решением, что byte-в-char должно быть явным преобразованием, и целое-в-диапазоне-char тоже должно преобразовываться явно.

    Отметим, что заметки по дизайну языка не указывают, почему ushort-в-char тогда же было сделано тоже явным, но, как видите, применима та же логика. Передавая ushort в метод, перегруженный как M(int) и M(char), вы скорее всего хотите трактовать ushort как число, а не как символ. А ushort не является представлением символа в том же смысле, как представлением числа, так что выглядит разумным сделать это преобразование таким же явным.

    Решение преобразовывать char в ushort неявно было принято 17 сентября 1999 года; заметки по дизайну за этот день на эту тему гласят просто «char в ushort тоже разрешённое неявное преобразование”, и это всё. Никаких других проявлений того, что происходило в головах дизайнеров языка в тот день в заметках не засвидетельствовано.

    Тем не менее, мы можем сделать обоснованные предположения причин, по которым неявное преобразование char в ushort было рассмотрено как хорошая мысль. Ключевая идея здесь в том, что преобразование из числа в символ является «возможно сомнительным». Оно берёт что-то, что не факт, что планировалось быть символом, и решает трактовать его как символ. Это похоже на тот тип действий, который вы хотели бы явно объявлять при выполнении, а не случайно позволять себе. Но обратное гораздо менее сомнительно. В программировании на C есть давняя традиция трактовать символы как целые числа – для получения их фактических значений, или для выполнения с ними математических операций.

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

  • Интернирование строк и String.Empty

    Вот любопытный фрагмент кода:

    object obj = "Int32";
    string str1 = "Int32";
    string str2 = typeof(int).Name;
    Console.WriteLine(obj == str1); // true
    Console.WriteLine(str1 == str2); // true
    Console.WriteLine(obj == str2); // false !?

    Конечно, если A равно B, и B равно C, то A равно C, это транзитивное своейство равенства. Похоже, что оно тут полностью нарушено.

    Ну, прежде всего, хотя транзитивность и желательна, это всего лишь одна из многих ситуаций, в которых равенство в C# нетранзитивно. Вы не должны полагаться на транзитивность в общем случае, хотя, конечно же, есть множество частных случаев, где она справедлива. В качестве упражнения, вы могли бы посмотреть, сколько других нетранзитивностей сможете вспомнить. (Случайно, один из вопросов, который мне задали на собеседовании при приёме в эту команду был про разработку производительного алгоритма для определения нетранзитивностей в упрощённой версии алгоритма «наилучшего метода».)

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

    Это может потребовать чуть больше объяснений. В .NET вы можете иметь две строки с идентичным содержимым, но они будут разными объектами. Когда вы сравниваете эти строки как строки, они равны, но когда вы сравниваете их как объекты, они не равны.

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

    Это результат маленькой оптимизации. Если у вас есть два идентичных строковых литерала в одной единице компиляции, то код, который мы генерируем, гарантирует, что только один объект строки создаётся CLR для всех экземпляров этого литерала в пределах сборки. Эта оптимизация называется «интернированием строк».

    String.Empty – не константа, это поле только-для-чтения в другой сборке. Поэтому оно не интернируется с пустой строкой в вашей сборке; это два разных объекта.

    Это объясняет, почему первое сравнение истинно: эти два литерала фактически превращаются в один и тот же объект строки. И это объясняет, почему третье сравнение ложно: литерал и вычисленное значение превращаются в разные объекты.

    Зная это, вы теперь можете сделать обоснованное предположение причины, по которой мы имеем это противоестественное поведение:

    object obj = "";
    string str1 = "";
    string str2 = String.Empty;
    Console.WriteLine(obj == str1); // true
    Console.WriteLine(str1 == str2); // true
    Console.WriteLine(obj == str2); // иногда true, иногда false?!

    Некоторые версии .NET автоматически интернируют пустую строку, некоторые – нет!

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

    Ответ – в том, что принцип ЛДНБ применим здесь в полной мере. То есть, Ланчей Даром Не Бывает. Интернирование имеет два положительных эффекта: оно уменьшает потребление памяти и уменьшает время сравнения двух строк. (Потому, что если все строки интернируются во время выполнения, то все сравнения строк могут быть дешёвыми сравнениями ссылок.) Но у этих положительных эффектов есть цена: выделение новой строки теперь требует от вас поиска по всем строкам в памяти для проверки, нет ли там уже такой. В нашей существующей оптимизации, цена невелика; во время компиляции нам известно, каковы строковые литералы в данной сборке и какие из них одинаковы. В предлагаемой оптимизации, эта цена платится во время выполнения, и может занимать весьма большую долю затрат времени на выделение строк.

    Чтобы снизить затраты времени, вам бы пришлось построить хеш-таблицу всех строк в памяти. Это означает либо частое вычисление хеш-кодов, что само по себе затратно по времени, либо их хранение где-то. Если мы выбираем последнее, то внезапно мы увеличиваем нагрузку на память для строк, которые не дублируются. То есть, наша оптимизация заставляет обычный сценарий – подавляющее большинство пар строк не совпадают друг с другом – требовать больше памяти, чтобы редкий сценарий мог её сэкономить. Это выглядит плохой сделкой; обычно вы хотите оптимизировать для более вероятного случая.

    Кроме того, у интернированных строк есть также и серьёзные проблемы с временем жизни. Когда их можно безопасно подвергнуть сборке мусора? Что, если новая копия строки создаётся в тот момент, когда старую удаляет сборщик в другом потоке? Самое безопасное – сделать интернированные строки бессмертными, что выглядит как утечка памяти. Утечки памяти плохо влияют на производительность, особенно когда ваша оптимизация – это попытка сэкономить память. ЛДНБ!

    Короче, в общем случае не стоит интернировать все строки. Тем не менее, это может оказаться стоящим в некоторых особых случаях. Например, если бы вы писали на C# компилятор, то вы бы скорее всего порождали в процессе выполнения большое количество одинаковых строк. Наш компилятор C# написан на C++, где мы написали нашу собственную прослойку интернирования строк, чтобы мы могли выполнять дешёвые ссылочные сравнения для всех строк в вашей программе. Весьма вероятно, что «int» будет встречаться десятки, сотни, или тысячи раз в одной программе; глупо выделять одну и ту же строку снова и снова. Если вы пишете на C# компилятор, или какое-то другое приложение, в котором вы чувствуете, что стоит постараться не дать тысячам идентичных строк потребить много памяти, то вы можете заставить среду исполнения интернировать вашу строку при помощи метода String.Intern.

    И, наоборот, если вы ненавидите интернирование слепой ненавистью, то можете заставить среду отключить всё интернирование строк в сборке при помощи атрибута CompilationRelaxation.

    В любом случае, вернёмся к вопросу транзитивности: равенство ссылок на объекты на самом деле транзитивно. Оно также симметрично (A==B подразумевает B==A) и рефлексивно (A==A), так что это отношение эквивалентности. Похожим образом, равенство значений строк транзитивно, симметрично и рефлексивно, поскольку оно использует прямолинейное сравнение «символ за символом». Но когда вы смешиваете их, то равенство перестаёт быть транзитивным. Это странно, но, надеюсь, теперь доступно для понимания.

  • Почему ковариантность массивов типов-значений несогласована?

    Еще один интересный вопрос со StackOverflow:

    uint[] foo = new uint[10];
    object bar = foo;
    Console.WriteLine("{0} {1} {2} {3}",       
      foo is uint[], // True
      foo is int[],  // False
      bar is uint[], // True
      bar is int[]); // True

    Что за ерунда тут происходит?

    Этот фрагмент кода иллюстрирует интересное, но неудачное противоречие между системой типов CLI и системой типов C#.

    В CLI есть концепция «совместимости по присваиванию». Если значение x известного типа S является «совместимым по присваиванию» с местом хранения y известного типа T, то вы можете записать x в y. Если нет, то попытка это сделать не является верифицируемым кодом и верификатор это запретит.

    Система типов CLI говорит, например, что подтипы ссылочного типа совместимы по присваиванию с супертипами ссылочного типа. Если у вас есть string, то её можно сохранить в переменной типа object, потому что оба типа – ссылочные, и string – подтип object. Но обратное неверно; супертипы не совместимы по присваиванию с подтипами. Вы не сможете засунуть что-то, известное как object, в переменную типа string без предварительного приведения типа.

    В сущности, «совместим по присваиванию» означает «имеет смысл засовывать эти конкретные биты в эту переменную». Присваивание исходных данных в переменную назначения должно «сохранять представление».

    Одно из правил CLI – в том, что «если X совместим по присваиванию с Y, то X[] совместим по присваиванию с Y[]».

    То есть, массивы ковариантны по отношению к совместимости по присваиванию. Как я уже обсуждал, это на самом деле сломанный вид ковариантности.

    Такого правила нет в C#. Правило ковариантности массивов в C# таково: «если X – ссылочный тип, неявно приводимый к ссылочному типу Y, то X[] неявно приводим к Y[]». Это слегка другое правило!

    В CLI, uint и int совместимы по присваиванию; так что uint[] и int[] тоже. Но в C#, преобразования между int и uint явные, а не неявные, и это типы-значения, а не ссылочные типы. Так что в C# запрещено конвертировать int[] в uint[]. Но это разрешено в CLI. Так что теперь мы стоим перед выбором.

    1) Реализовать “is” так, чтобы, когда компилятор не смог статически определить результат, то он вставлял бы вызов метода, который проверяет все правила C# по проверке конвертируемости, сохраняющей представление. Это медленно, и в 99.9% случаев совпадает с результатом применения правил CLI. Но мы принимаем потерю производительности для 100% совместимости с правилами C#.

    2) Реализовать “is” так, что когда компилятор не смог статически определить результат, то он полагался бы на невероятно быструю проверку совместимости по присваиванию из CLR, и жить с тем фактом, что она говорит, что uint[] это int[], несмотря на то, что это на самом деле не так в C#.

    Мы выбрали последнее. Не очень хорошо, что спецификации C# и CLI расходятся в этом мелком вопросе, но мы готовы жить с этим противоречием.

    Так что здесь происходит то, что в случаях «foo», компилятр статически может определить, каков будет результат в соответствии с правилами C#, и генерирует код для порождения «True» и «False». Но в случаях «bar», компилятор уже не знает точный тип того, что лежит в bar, так что он генерирует код, чтобы заставить CLR отвечать на вопрос, и CLR высказывает другое мнение.

  • Почему в ref и out параметрах нет вариантности типов?

    Вот хороший вопрос со StackOverflow:

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

    Предположим, что у вас есть классы Животное, Млекопитающее, Рептилия, Жираф, Черепаха и Тигр, с очевидными отношениями наследования.

    Теперь предположим, что у вас есть метод void M(ref Млекопитающее m). M может как читать, так и писать m. Можно ли передать в M переменную типа Животное? Нет. Это было бы небезопасно. Такая переменная может ссылаться на Черепаху, но M предполагает, что там могут быть только Млекопитающие. Черепаха – не млекопитающее.

    Вывод 1: Ref-параметры нельзя делать «больше». (Животных больше, чем млекопитающих, так что переменная становится «больше» потому, что в неё входит больше разных существ)

    Можно ли передать в M переменную типа Жираф? Нет. M может записывать в m, и может захотеть записать туда экземпляр Тигра. Теперь вы засунули Тигра в переменную, которая имеет тип Жираф.

    Вывод 2: Ref-параметры нельзя делать «меньше».

    Теперь рассмотрим N(out Млекопитающее n).

    Можно ли передать в N переменную типа Жираф? Нет. Как и в нашем предыдущем примере, N может записывать в n, и N может захотеть записать туда Тигра.

    Вывод 3: Out-параметры нельзя делать «меньше».

    Можно ли передать в N переменную типа Животное?

    Хмм.

    Ну, почему бы и нет? N не может читать из n, он может туда только писать, верно? Вы записываете Тигра в переменную типа Животное и всё в порядке, так?

    Нет. Правило не в том, что «N может только записывать в n». Правила, вкратце, таковы:

    1) N обязан записать в n перед тем, как выполнить нормальный возврат (если N бросает исключение, то с него взятки гладки)

    2) N обязан записать что-то в n перед тем, как что-то оттуда прочитать.

    Это позволяет такую последовательность событий:

    • Объявляем поле x типа Животное.
    • Передаём x как out-параметр в N
    • N пишет Тигра в n, который является псевдонимом для x.
    • В другом потоке, кто-то записывает в x Черепаху.
    • N пытается читать содержимое n, и обнаруживает Черепаху в том, что, по его мнению, является переменной типа Млекопитающее.

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

    UPDATE: Комментатор Павел Минаев верно подметил, что нет нужды в многопоточности для нанесения увечий. Мы можем заменить четвёртый шаг на

    • N делает вызов метода, который прямо или косвенно заставляет некоторый код записать в x Черепаху.

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

    Вывод 4: Out-параметры нельзя делать «больше».

    Есть и другой аргумент в пользу этого вывода: «out» и «ref» на самом деле за кулисами совершенно одинаковы. CLR поддерживает только «ref»; «out» - это всего лишь «ref», для которого компилятор навязывает несколько другие правила насчёт того, когда рассматриваемая переменная подвергается определяющему присваиванию. Вот почему запрещено делать перегрузки метода, которые отличаются только out/ref-ностью параметров; CLR неспособен их различить! Так что правила типобезопасности для out вынуждены быть такими же, как и для ref.

    Окончательный вывод: Ни ref ни out параметры не позволяют варьировать типы аргументов в местах вызова. Иное бы нарушило верифицируемую безопасность типов.

  • К западу от здания

    К западу от здания
    Вы стоите в открытом поле к западу от белого здания с дощатой входной дверью.
    Тут маленький почтовый ящик.

    >открыть ящик
    В открытом маленьком почтовом ящике обнаружен листок.

    >взять листок
    Взят.

    >прочитать листок
    «ДОБРО ПОЖАЛОВАТЬ В ZORK!

    ZORK – игра, полная приключений, опасностей, и хитростей. В ней вы исследуете часть самой потрясающей территории, когда-либо доступной смертным. Ни один компьютер не должен быть её лишён

    И так в 1984 началось моё пожизненное увлечение «интерактивной литературой». Где-то в ящике для документов моего офисного шкафа у меня есть сотни нарисованных на миллиметровке вручную карт из квадратиков и линий, описывающих для меня план каждой из классических игр Infocom, где там каждый объект, и так далее.

    В 1984 мне было одиннадцать лет, и я наивно полагал себя прямо чертовски продвинутым программистом. Я уже написал к тому моменту несколько простых игр для Commodore PET (классического CBM 4032) в школе, добыл свой собственный Commodore 64 – благодаря маме – и обнаружил и исправил баг в программе, написанной профессионалами. Во как! Но я ни в жизнь не мог догадаться, каким образом разработчики в Infocom написали такую огромную, сложную игру, которая могла понимать предложения на английском, поддерживать согласованные положения иерархических объектов, которые взаимодействуют с окружающей средой (то есть, факел - в гробу, гроб – в лодке, лодка скользит вниз по течению), и так далее.

    Когда через несколько лет я узнал, как это всё работало, я просто обалдел. Это был один из тех моментов, когда вы вдруг ясно понимаете, что есть целый новый способ воспринимать компьютеры, как инструменты для решения задач. Как теперь хорошо известно, то, что сделали гении в Infocom, было разработкой и реализацией их собственной виртуальной машины, Z-машины. Затем они написали игры на байткоде этой виртуальной машины. Это даёт два огромных преимущества.

    Во-первых, абстрагируясь от реальной машины, вы можете написать вашу огромную сложную игру один раз, написать относительно маленькие и простые реализации Z-машины для какого хотите количества марок компьютеров, и внезапно вы получаете возможность написать-однажды-запускать-везде, значительно увеличивая количество платформ, для которых вы можете продавать. Большинство видеоигр того времени были написаны, скажем, на ассемблере Commodore 64, а потом переписаны на ассемблер Atari, и так далее; цена линейно масштабировалась относительно количества платформ. В подходе Infocom, затраты на каждую платформу были намного ниже.

    Во-вторых, ВМ может реализовывать то, что мы сейчас воспринимаем как страничная организация памяти. (Неизменяемый) код игры можно читать с диска по странице за один раз, исполнять, и затем выбрасывать. Единственное, что нужно держать в памяти, это относительно маленькое текущее состояние игры и, конечно, саму реализацию Z-машины. Но код может быть огромным, значительно больше доступной памяти. В те времена, когда доступной памяти было от 16 до 32 килобайт, это было значительным преимуществом.

    В наши дни, конечно, множество людей написали свои реализации Z-машины для собственного удовольствия. Я этого так и не сделал, но всегда хотел. После работы над столь многими языками с интерпретируемым байткодом за последние пятнадцать лет, это, вероятно, было бы достаточно несложным. Это был бы заметный объём работы, но об этом было бы занятно писать в блоге, и, я уверен, там было бы множество отличных возможностей проиллюстрировать изготовление настоящего интерпретатора байт-кода. Но, с другой стороны, у меня уже есть много того, чем можно занять своё свободное время, и склонность откусывать больше, чем я могу проглотить в один приём.

    Так что весьма удачно, что мне не нужно этого делать, потому что Майк Грегер делает именно это. Он уже написал несколько реализаций Z-машины на C# и пишет в блоге об этом процессе. Я с нетерпением предвкушаю чтение блога Майка и выяснение скрытых технических и исторических деталей, которые делают Z-машину столь интересной. Майк говорит мне, что у него трудности с поиском людей, кто в восторге и от C#, и от Z-машины; ладно, очевидно – засчитывай меня! Я уверен, что некоторые из моих читателей тоже заинтересованы и в том, и в другой.

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

    Так же, как «fixed» и «into», «partial» используется в С# двумя похожими-но-разными способами.

    Задача частичного класса в том, чтобы позволить вам разбивать объявление класса на несколько частей, обычно расположенных в различных файлах. Мотиватором этой возможности был машинно-генерируемый код, который пользователю нужно было расширять путём прямых добавлений. Когда вы рисуете форму в дизайнере форм, дизайнер генерирует для вас класс, представляющий эту форму. Вы можете затем далее изменять этот класс, добавляя в него новый код. Если вы можете редактировать код, сгенерированный машиной, то возникает масса проблем. Что, если он будет сгенерирован повторно? Что, если машина использует сам код для хранения информации о дизайне формы, и ваши правки сбивают с толку парсер машины? Гораздо лучше просто поместить машинно-генерируемую половину в отдельный файл, сгенерировать комментарий «не трогайте это», и разместить пользовательский код в другом месте.

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

    • Если класс действительно большой, и реализует пачку интерфейсов, то, иногда, реализация каждого интерфейса в отдельном файле имеет смысл. Хотя, чаще это признак плохого кода, присущий классу, который пытается делать слишком много всего; разделение этой штуки на несколько классов может оказаться лучше.
    • Иногда приятно выносить вложенные классы в отдельные файлы; единственный приличный способ этого добиться – это сделать содержащий их класс частичным.
    • И так далее

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

    Способ, которым работают частичные методы – это наличие двух объявлений, «латентного», и «актуального». У латентного объявления нет тела, как будто это абстрактный метод. У актуального – есть. Латентное объявление располагается на машинно-сгенерированной стороне частичного класса, актуальное объявление попадает в часть, написанную человеком. Если у нас есть актуальное объявление, то латентное полностью игнорируется. Но если актуального объявления нет, то все вызовы метода устраняются, как будто он был скомпилирован с условным атрибутом! И, фактически, латентное объявление также устраняется в момент порождения метаданных для сгенерированного класса; как будто его и не было.

    Причина для такого поведения – в том, что мы хотели сделать возможным такой сценарий:

    // Машинно-генерируемый код:
    partial class MyFoo
    {
    void ButtonClickEventHandler(/*всякое*/)
    {
    // вызвать пользовательский код чтобы посмотреть, не хочет ли он чего-нибудь сделать
    OnBeforeButtonClick(всякое);
    тра ля ля
    // опять вызвать пользовательский код
    OnAfterButtonClick(всякое);
    }
    partial void OnBeforeButtonClick(/*всякое*/);
    partial void OnAfterButtonClick(/*всякое*/);
    ...

    Пользователь будет модифицировать частичный класс; вставляя частичные методы, машинно-сгенерированная часть может повсеместно создать простые точки расширения, которые пользователь потом сможет реализовать. Но подумайте о негативных последствиях этого в мире без частичных методов. Пользователя заставляют реализовывать пустые методы. Если он этого не сделает, то получит ошибку. Потенциально там нужно реализовать сотни этих пустых методов, а это бесит. И каждый из этих методов генерирует нетривиальное количество метаданных, делая результирующую библиотеку больше, чем нужно. Дисковое пространство дёшево, но латентность сети заменила дисковое пространство в качестве фактора против больших сборок. Было бы здорово, если бы мы могли избавиться от этого груза метаданных.

    Частичные методы удовлетворяют требованиям. Пользователь может предоставлять реализации для методов по собственному выбору, и это становится моделью с «платой за фактическое использование»; вы получаете столько расходов на реализацию, сколько методов вы реализуете.

    Эта возможность во время процесса проектирования называлась «латентные и актуальные методы»; мы серьёзно рассматривали добавление новых ключевых слов «latent» и «actual». Но, поскольку эта возможность имеет смысл только в сценариях с частичными классами, то мы решили повторно использовать существующее контекстное ключевое слово «partial» и переименовали возможность. Надеюсь, что созвучие между двумя применениями «partial» помогает больше, чем тонкая разница вредит.

    СУПЕР ЭКСТРА БОНУС: Ещё немного частичности

    Мы рассматривали добавление третьего типа «частичности» в C# 4.0; эта возможность прошла через фазу проектирования, но была выброшена до реализации. (Если будет высокий спрос, то мы рассмотрим добавление её в гипотетических будущих версиях C#.)

    Иногда вы участвуете в сценарии машинной генерации кода типа такого:

    // сгенерировано машиной
    partial class C
    {
    int blah;
    ...

    И затем на той стороне, которую генерирует пользователь, вы хотите сделать что-то вроде:

    // User generated
    partial class C : ISerializable
    {

    И, о, чёрт, мне нужно пометить blah атрибутом NotSerialized, но я не могу редактировать текст blah потому, что когда его перегенерируют, это всё пропадёт.

    Идея этого нового типа частичности – в том, чтобы повторять объявление члена – поля, метода, вложенного типа, чего угодно – с атрибутами метаданных. Это как латентные/актуальные методы, только наоборот; «актуальная» часть – на машинно-генерируемой стороне, а «латентная» часть на стороне, генерируемой пользователем, нужна только для добавления метаданных:

    [NotSerialized] partial int blah; // на самом деле не объявляет поле

    Мне нравится такая возможность, но во время процесса проектирования я сопротивлялся, как мог, использованию ключевого слова «partial» в третьем значении, слегка отличающемся от первых двух. Добавление такой путаницы один раз казалось оправданным, но дважды? Это перевешивает. Так что мы сошлись на добавлении другого контекстного ключевого слова:

    [NotSerialized] existing int blah; // на самом деле не объявляет поле

    Оформление объявления при помощи «existing» значило бы «это не настоящее объявление, это упоминание существующего объявления; пожалуйста, проверьте, что такое объявление существует где-то еще в этом частичном классе, и добавьте эти метаданные к тому члену».

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

  • Извините за CAPTCHA

    Быстрая заметка по метаблоггингу. Те из вас, кто комментирует этот блог (6700+ комментариев и продолжают поступать, спасибо вам) уже, вероятно, заметили, что теперь тут есть CAPTCHA, тот маленький тест «пожалуйста, докажите, что вы человек» перед тем, как отправить комментарий.

    Я понимаю причины. Сайты блогов MSDN и TechNet являются ценными целями для нежеланных коммерческих рекламодателей, для атакующих, которые хотят вынудить поисковые движки направить трафик на их сайты, и вандалов. У людей, которые занимаются безопасностью этого сайта, полно работы; мы уже испытывали некоторые достаточно серьёзные отказы в обслуживании из-за атак криворуких спамеров. Добавление CAPTCHA для проверки комментариев существенно снижает процент успешного спама в комментариях.

    Я не восторге от этого. Я нахожу решения в стиле CAPTCHA безвкусицей по нескольким причинам:

    • Добронравный комментатор – в точности тот персонаж, которого мы хотим поощрять – вынужден делать лишнюю работу. Это маленькое, но ненулевое препятствие к написанию комментариев
    • Иногда будут происходить ошбки; предоставление компьютерам новых способов ежедневно напоминать нам, что мы неудачники, выглядит раздражающим
    • Презумпция невиновности сменяется презумпцией вины; добронравный комментатор обязан доказывать свою невинность. Каждый раз, когда мне нужно заполнить CAPTCHA, я чувствую маленькое, но реальное оскорбление; я надёжный человек, так что уже доверяйте мне. Как однажды заметил Джоель Спольски, это как первой вещью при входе на станцию увидеть знак «КАТАНИЕ НА СКЕЙТАХ ЗАПРЕЩЕНО ПОПРОШАЙНИЧЕСТВО ЗАПРЕЩЕНО ТО ЗАПРЕЩЕНО СЁ ЗАПРЕЩЕНО ЭТО ТОЖЕ ЗАПРЕЩЕНО». Это негостеприимно. Это заставляет вас чувствовать вину и напоминает, что в мире есть зло.
    • Есть проблемы с доступностью. Не все, кто пользуется компьютерами, имеют идеальное зрение, но это не делает их злобными роботами. Они заслуживают таких же шансов на внесение своего вклада, как и все остальные, и уже вынуждены преодолевать множество препятствий; не стоит подбрасывать им новых.
    • И так далее
    Так что, извините за это, комментаторы. Мне это нравится не больше, чем вам, но я не так уж много могу по этому поводу сделать; не я управляю серверами блогов. Единственная вещь, которой я управляю – это насколько фиолетовым будет цвет.
  • В чём разница между условной компиляцией и атрибутом Conditional?

    Пользователь: почему эта программа отказывается компилироваться в релизном билде?

    class Program
    {
    #if DEBUG
        static int testCounter = 0;
    #endif
        static void Main(string[] args)
        {
            SomeTestMethod(testCounter++);
        }
        [Conditional("DEBUG")]
        static void SomeTestMethod(int t) { }
    }

    Эрик: Это не получается скомпилировать при окончательном построении потому, что не удаётся найти testCounter при вызове SomeTestMethod.

    Пользователь: Но этот вызов всё равно будет выброшен, так почему это имеет значение? Явно есть какая-то разница между устранением кода при помощи условной компиляции и при помощи условного атрибута, но в чём эта разница?

    Эрик: Тебе уже известен ответ на твой вопрос, просто ты об этом еще не знаешь. Давай поиграем в Сократа; я верну вопрос тебе – как это работает? Откуда компилятор узнает, что нужно устранить вызов метода?

    Пользователь: Потому, что вызываемый метод помечен атрибутом Conditional.

    Эрик: Это знаешь ты. Но откуда компилятор знает, что вызываемый метод помечен атрибутом Conditional?

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

    Эрик: Ясно. То есть, фундаментально, основная работа делается разрешением перегрузок. А откуда разрешение перегрузок знает, что нужно выбрать именно тот метод? Предположим гипотетически, что у нас там был и другой метод с тем же именем и другими параметрами.

    Пользователь: Разрешение перегрузок работает путём изучения аргументов вызова и сравнения их с типами параметров каждого метода-кандидата, и затем выбора единственного лучшего соответствия среди всех кандидатов.

    Эрик: Вот и оно. Таким образом, аргументы должны быть полностью определены в точке вызова, даже если вызов будет впоследствии устранён. Фактически, вызов невозможно устранить, не имея в наличии аргументов! Но в релизном билде, тип аргумента невозможно определить, потому что его объявление было выброшено.

    Так что теперь вы видите, что реальная разница между этими двумя техниками устранения нежелательного кода а том, что делает компилятор в момент устранения. На высоком уровне, компилятор обрабатывает текст так. Сначала он «лексит» файл. То есть, разбивает строку на «лексемы» - последовательности букв, цифр и символов, которые имеют смысл для компилятора. Затем эти лексемы «разбираются», чтобы проверить соответствие программы грамматике C#. Затем разобранное состояние анализируется для определения семантической информации о нём; каков тип каждого выражения и всё такое. И, наконец, компилятор выплёвывает код, который эту семантику реализует.

    Действие директивы условной компиляции происходит во время лексического разбора; всё, что попало внутрь устранённого блока #if трактуется лексическим анализатором как комментарий. Как будто вы просто удалили всё содержимое блока и заменили пробелом. Но устранение вызовов в зависимости от условных атрибутов происходит во время сематического анализа; всё необходимое для проведения этого семантического анализа должно быть в наличии.

    Пользователь: Потрясающе. Какие разделы спецификации C# определяют это поведение?

    Эрик: Спецификация начинается с удобного «содержания», которое очень помогает в ответах на такие вопросы. Содержание утверждает, что секция 2.5.1 описывает «Символы условной компиляции», а секция 17.4.1. описывает «атрибут Conditional».

    Пользователь: Обалдеть.

  • Какая разница, часть Пятая: подписи сертификатами и строгие имена

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

    Наиболее важные их различия лежат не в математических подробностях, а в том, какие проблемы они призваны решить.

    Единственная цель строгого имени – в гарантии того, что когда вы загружаете сборку по имени, вы загружаете именно ту сборку, которую думаете, что загружаете. Вы говорите «я хочу загрузить Frobber версии 4, который разработан FooCorp». Механизм строгого имени гарантирует, что вы загружаете именно эту DLL, а не другую сборку по имени Frobber версии 4, которая изготовлена Доктор Зло, Инк. Теперь вы можете установить политику безопасности, гласящую «если у меня на машине есть сборка производства FooCorp, то доверяй ей полностью». Эти сценарии - единственные задачи, для которых проектировались строгие имена.

    Всё, что вам нужно для обеспечения этого - знать маркер публичного ключа, ассоциированный с приватным ключом компании FooCorp. Способ, которым вы получите этот маркер публичного ключа, – это ваше личное дело. Нет никакой готовой инфраструктуры, спроектированной для обеспечения безопасного получения вами этой информации. От вас просто ждут, что вы каким-то образом это узнаете. Если злые люди обманом могут заставить вас поверить, что маркер их ключа на самом деле – маркер ключа компании FooCorp, то у вас проблема. От вас ожидают изобретения какого-то разумного способа определить, каков на самом деле маркер ключа FooCorp.

    Цель цифровой подписи сертификатом издателя – в установлении верифицируемой цепочки идентификации и доверия. Цепочка доверия ведёт от куска кода неизвестного или непроверенного происхождения к «доверенному корню» - субъекту, доверие к которому вы настроили в своей операционной системе.

    Вы скачиваете некоторый код, и у кода есть цифровая подпись сертификатом, принадлежащим FooCorp. Вы смотрите в сертификат, и он говорит «эта программа пришла от FooCorp. Точность этого сертификата удостоверена VeriSign». Поскольку Verisign является одним из ваших доверенных корней, то теперь у вас есть уверенность в том, что этот код действительно пришёл от FooCorp.

    Заметьте, насколько сложнее проблема, решаемая цифровыми подписями. Мы не просто пытаемся определить «ассоциирован ли с данным именем этот кусок кода?» Вместо этого мы пытаемся определить, откуда взялся этот код, и кто подтверждает существование якобы ответственной за него компании, и стоит ли нам доверять этой компании.

    Разница между строгими именами и цифровыми подписями подчёркивает сложности безопасности, основанной на криптографии. Сложная задача не в криптографии; это всего лишь математика. Сложная задача – безопасное управление распределением информации о ключах и ассоциация их с корректными субъектами. Строгие имена не имеют этих проблем с управлением ключами, поскольку стараются решать очень маленькую, но важную задачу. Или, скорее, они перекладывают задачу управления ключами на вас, на пользователя. Всё, ради чего нужны цифровые подписи – это попытка автоматизировать безопасное распределение информации о ключах при помощи сертификатов, для решения значительно более сложных задач доверия и идентификации.

  • Какая разница, часть Четвёртая: into и into

    Ключевое слово «into» в выражениях-запросах означает две разных вещи, в зависимости от того, идёт ли оно после join или select/group. Если оно следует за join, то оно превращает объединение в групповое объединение. Если оно следует за select или group, то оно вводит продолжение запроса. Эти две вещи сильно отличаются, но их легко спутать.

    Во-первых, групповое объединение. Предположим, у вас есть ключ – идентификатор покупателя – который используется в качестве первичного ключа коллекции покупателей, и в качестве внешнего ключа в коллекции номеров кредитных карточек. То есть, у вас есть класс Customer с полями Id, Name, Address, и так далее, и класс CreditCard с полями CustomerId, CardType, Number, и так далее. Пусть у покупателя Боба есть Виза и Дискавер, а у покупателя Алисы есть Виза и Мастеркард. Так что у нас есть данные о покупателях:

    101, Bob
    102, Alice

    и данные кредиток:

    101, Visa
    101, Discover
    102, Visa
    102, MasterCard

    Если мы построим запрос

    from customer in customers
    join card in cards on customer.Id equals card.CustomerId
    select new {customer.Name, card.Kind}

    То результатом будет

    Bob, Visa
    Bob, Discover
    Alice, Visa
    Alice, Mastercard

    Верно? Это всего лишь прямолинейное объединение. Мы заканчиваем списком из четырёх элементов. Но это, вероятно, не то, что вы на самом деле хотите в этом случае. Предположим, что вы хотели список покупателей, и для каждого покупателя в списке, список их кредиток. Вы можете использовать групповое объединение:

    from customer in customers
    join card in cards on customer.Id equals card.CustomerId into cardList
    select new {customer.Name, Cards = cardList}

    Результатом этого запроса были бы две записи, а не четыре:

    Bob, { Visa, Discover }
    Alice, { Visa, Mastercard }

    В основном, «into» в групповом объединении – это порция выражения-запроса, которая логически собирает результаты всех объединённых записей, объединяет их в последовательность, и запихивает их во временную переменную cardList.

    Продолжение запроса означает совсем другое. Смысл продолжения запроса в упрощении «передачи» результатов одного запроса в следующий запрос. Например, предположим, что вы хотите найти всех кареглазых детей, у кого есть хотя бы один голубоглазый брат или сестра. Очевидный способ сделать это – что-то вроде

    from parent in parents
    from child in parent.Children
    where child.EyeColor == "Brown"
    where parent.Children.Any(c=>c.EyeColor == Blue)
    select child

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

    var parentsWithABlueEyedChild =
        from parent in parents
        where parent.Children.Any(c=>c.EyeColor == Blue)
        select parent;
    var brownEyedChildren =
        from p in parentsWithABlueEyedChild
        from child in p.Children
        where child.EyeColor == Brown
        select child;

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

    var brownEyedChildren =
        from p in (
            from parent in parents
            where parent.Children.Any(c=>c.EyeColor == Blue)
            select parent)
        from child in p.Children
        where child.EyeColor == Brown
        select child;

    Но... Представьте чуть больше уровней вложенности. Это превратится в бардак. Заметьте, как мы ввели переменную диапазона «p» вначале, а потом нам пришлось пробраться через весь второй запрос прежде, чем снова её употребить. Здесь мы вводим переменные в «обратном» порядке. Продолжение запроса просто позволяет вам изменить этот порядок обратно на «прямой», перемещая переменную диапазона p в конец первоначального запроса:

    var brownEyedChildren =
        from parent in parents
        where parent.Children.Any(c=>c.EyeColor == Blue)
        select parent into p
        from child in p.Children
        where child.EyeColor == Brown
        select child;

    Заметьте, что в случае группового объединения, можно считать идентификатор справа от «into» логически представляющим последовательность, полученную в результате группировки соответствующих присоединённых элементов. Но в случае продолжения запроса, «into»-идентификатор не соответствует последовательности из первого запроса – сам запрос и есть объект, представляющий первую последовательность! Вместо этого, «p» представляет переменную диапазона, которая соответствует одному элементу коллекции за раз. Помните, «into» в продолжении запроса – всего лишь модный способ сказать from p in (blah); «p» - это переменная диапазона, которая по очереди проходит элементы (blah), но не сама последовательность элементов (blah).

  • Какая разница, часть Третья: fixed и fixed

    Вчера получил письмо, которое начиналось так:

    У меня есть вопрос про буфера фиксированного размера в C#:

    unsafe struct FixedBuffer { public fixed int buffer[100]; }

    Поскольку я объявил buffer как fixed, его нельзя перемещать…

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

    При выполнении в небезопасном коде арифметики указателей над управляемым объектом, вам нужно гарантировать, что сборщик мусора не переместит память, на которую вы смотрите. Если сборка мусора случится в другом потоке в тот момент, когда вы выполняете арифметику указателей над объектом, указатели могут съехать. Поэтому, C# классифицирует все переменные как либо «зафиксированные», либо как «перемещаемые». Если вы хотите заниматься арифметикой указателей над перемещаемым объектом, вы можете использовать ключевое слово «fixed», чтобы сказать «эта локальная переменная содержит данные, которые сборщик мусора двигать не должен». При сборке мусора, сборщику нужно посмотреть на все локальные переменные всех выполняемых вызов (потому, конечно же, что все объекты в этих локальных переменных должны оставаться живыми); если он видит локальную переменную, помеченную «fixed», то он отмечает себе запрет перемещать адресованную ею память, даже если это приведёт к фрагментации управляемой кучи. (Вот почему важно держать объекты зафиксированными как можно менее долго.) Так что обычно мы используем «fixed» в смысле «зафиксирован на месте».

    Но это не то, что означает «fixed» в данном контексте; тут оно означает «обсуждаемый буфер имеет фиксированный размер в одну сотню целых» - в некотором смысле, это эквивалентно генерации сотни целых полей в этой структуре.

    Очевидно, мы часто используем одно ключевое слово для обозначения концептуально одинаковых вещей. Например, в C# мы многими способами применяем ключевое слово «internal», но все они концептуально одинаковы. Оно используется только для обозначения «доступ к некоторой сущности разрешён всему коду этой сборки».

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

    var results = from c in customers where c.City == "London" select c;

    и

    class C<T> where T : IComparable<T>

    Должно быть, понятно, что «where» используеся двумя совершенно разными способами: для построения фильтра в запросе, и для объявления ограничения на параметр обобщённого типа.

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

    Ну, тут можно сказать, что это всего лишь неудачное совпадение терминов; что «фиксированного размера» и «фиксированного расположения» просто случайно используют слово «fixed» в разных смыслах, вот ведь незадача.

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

    С одной стороны, гораздо понятнее было бы использование двух ключевых слов, скажем «pinned» и «fixed». Но, с другой стороноы, оба использования «fixed» разрешены только в небезопасном коде. Ключевое предположение насчёт всех возможностей небезопасного кода – в том, что если вы готовы писать небезопасный код на C#, то вы уже программист-эксперт, который полностью понимает управление памятью в CLR. Вот почему мы заставляем вас помечать код «unsafe»; это указывает, что вы отключаете систему безопасности и знаете, что делаете.

    Заметная доля ключевых слов C# применяются двумя или более способами: fixed, into, partial, out, in, new, delegate, where, using, class, struct, true, false, base, this, event, return и void – все имеют как минимум два различных значения. Большинство из них понятны из контекста, но, по крайней мере, первые три – fixed, into и partial – вызвали достаточно недоумений, чтобы я получил вопросы про их различия от озадаченных пользователей. Следующими я рассмотрю «into» и «partial».

  • Блоки итераторов, часть Седьмая: Почему нет анонимных итераторов?

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

    Почему мы не разрешаем анонимные итераторы? Я был бы счастлив иметь анонимные блоки итераторов. Я хочу писать что-то вроде:

    IEnumerable<int> twoints = ()=>{ yield return x; yield return x*10; };
    foreach(int i in twoints) ...

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

    Затраты велики. Переписывание итераторов – это самое сложное преобразование в компиляторе, а переписывание анонимных методов – второе по сложности. Анонимные методы могут быть вложены в другие анонимные методы, и анонимные методы могут быть внутри блоков итераторов. Так что, первое что мы делаем, это переписываем все анонимные методы так, чтобы они стали методами класса-замыкания. Это предпоследнее, что делает компилятор перед генерацией IL для метода. Как только этот шаг выполнен, переписчик итераторов может предполагать, что в блоке итератора нет анонимных методов; все они уже были переписаны. Так что переписчик итераторов может сконцентрироваться на переписывании итератора, не беспокоясь о том, что там могли остаться нереализованные анонимные методы.

    Кроме того, блоки итераторов никогда не вкладываются друг в друга, в отличие от анонимных методов. Переписчик итераторов может предполагать, что все блоки итераторов находятся «на верхнем уровне».

    Если позволить анонимным методам включать блоки итераторов, то оба этих предположения можно выкинуть в окно. У вас может быть блок итератора, который содержит анонимный метод, который содержит анонимный метод, который содержит блок итератора, который содержит анонимный метод, и… буээ. Теперь нам нужно написать проход переписывания, который может обрабатывать вложенные блоки итераторов и вложенные анонимные методы одновременно, объединив наши два самых запутанных алгоритма в один намного более запутанный алгоритм. Это было бы реально трудно спроектировать, реализовать, и протестировать. Я уверен, мы достаточно умны для этого. У нас тут сообразительная команда. Но мы не хотим брать на себя этот тяжкий труд ради «полезной, но не необходимой» возможности.

    оригинал статьи

  • Теория массового обслуживания в действии, плюс лягушки

    Ну, это был прекрасный отпуск. Начался он плохо, но затем значительно улучшился.

    Предположим, у вас есть «вход», который поставляет некоторое большое количество «потребителей» по некоторому расписанию. У вас есть несколько «служащих», которые обрабатывают запросы потребителей. Как только запрос потребителя выполнен, потребитель выходит через «выход». Что происходит, когда подряд приходят больше потребителей, чем у вас есть служащих, готовых их обслужить? Это область аналитической теории массового обслуживания; эта теория лежит в основе огромного количества человеческих и технических задач, от получения двойного чизбургера с луковыми кольцами и большим… оранжевым… напитком… до маршрутизации телефонных звонков в спутниковых сетях.

    (Некоторым из вас, возможно, любопытно, какое отношение первые два абзаца имеют друг к другу. Скоро всё сойдётся, обещаю.)

    Интересный пример двух разных алгоритмов обслуживания очередей предоставлен двумя популярными сетями ресторанов быстрого питания. В ресторане «M», если, там, к примеру, четыре кассира, то будет четыре очереди. Посетитель приходит, выбирает очередь и ждёт. В ресторане «W» есть одна длинная змеистая очередь; когда освобождается кассир, человек в голове этой очереди идёт к этому кассиру.

    Принципиальный недостаток системы W – в том, что та единственная очередь выглядит так, что стоять в ней намного дольше, чем в четырёх коротких очередях в системе M, что может отпугивать. Но практически по любой уместной объективной метрике, практически по любому уместному социальному фактору, и практически в каждом типичном бизнес-сценарии реального мира система W является предпочтительной:

    • Система W не требует от потребителя принимать решение на основе неполной информации; система M в своей основе предлагает потребителю играть в рулетку. Какая очередь быстрее всех? Это зависит не только от компетентности кассира, но и от того, просты или сложны транзакции, ожидающие в данной очереди.
    • Предположим, вы в системе M и две очереди – ваша, и та, что рядом – обслуживаются примерно с одинаковой средней скоростью. Вполне возможно, и быть может, даже естественно, что, несмотря на одинаковую скорость движения , что в связи с внезапными движениями и остановками в обеих очередях вам будет казаться, что люди рядом с вами «обгоняют» вас чаще, чем вы «обгоняете» их. Весьма возможно, что люди в обеих очередях будут иметь это ощущение одновременно! Каждый чувствует, что выбрал неправильную очередь, даже если в среднем нет «неправильной очереди». В системе W есть только одна очередь, так что она правильная автоматически.
    • Система W – честная; потребитель, ждавший дольше всех, всегда обслуживается следующим. Система M – нечестная; потребитель, попавший в «быструю» очередь может обслужиться до того, как потребители в «медленной» очереди, где они ждут окончания сложной продолжительной транзакции (или, хуже, ждут потребителя, который достиг головы очереди до того, как решил, что заказать).
    • Система W в теории имеет более высокую пропускную способность; единственный случай, когда потребитель с быстрой транзакцией вынужден долго ждать в голове очереди – это та маловероятная ситуация,что все кассиры оказались заняты сложными транзакциями. Если любой кассир сейчас обслуживает быстрые транзакции, то он быстро опустошает голову очереди. В системе M, много быстрых транзакций могут быть задержаны единственноймедленной. Это сильно снижает среднюю пропускную способность.
    • Система W намного гибче. Новых кассиров можно динамически добавлять, когда очередь становится слишком длинной, и отправлять заниматься менее тяжёлой работой, когда она укорачивается. В системе M, когда новый кассир включается в работу, может случиться неорганизованная спешка для формирования новой очереди; потребителей заставляют снова принять решение – попробовать ли новую очередь или остаться в старой, и это предоставляет новые возможности для кажущейся нечестности. Но, хуже, когда кассир в системе M уходит, то что происходит с его очередью?

    Именно важность последнего вопроса была подчёркнута для меня в первый день моего отпуска. Назовём мою авиакомпанию «D». Оформление багажа пассажиров авиакомпании D в Международном Аэропорту Сиэтл-Такома работает по модели массового обслуживания “M”. Вы регистрируетесь в одном из киосков, печатаете посадочные талоны, платите ваши пятнадцать долларов за оформление чемодана, и выбираете одну из примерно десяти очередей.

    Ну, как минимум в теории D устранила одну из проблем модели M; последнее, что говорит вам система регистрации – это в какую очередь вставать. Я не знаю, как именно эта система решает, какая из очередей лучше, или читает ли хоть кто-то эти сообщения. Это малозаметная штука; лично я не ожидал, что система выдаст мне эту информацию, так что мне легко представить, что кто-то может полностью её пропустить и по-старинке выбрать любую очередь.

    Но так или иначе, мы стоим в нашей назначенной очереди и она потихоньку движется. Нам не слишком важно, сколько времени это занимает, потому что наш рейс задержали, предположительно, на один час, в связи с «непредвиденной технической проблемой». Так что у нас полно времени. (Как выяснилось, в действительности у нас было более трёх часов дополнительного времени. Я не против. Авиамеханики, которые это читают: пожалуйста, тратьте столько времени, сколько вам нужно, чтобы гарантировать, что крылья не отвалятся.) И, пока мы ждём, я указываю на кое-что своей жене: лента конвеера, которая должна увозить багаж, не движется. Или, скорее, движется – примерно на один чемодан в минуту – резко стартуя и останавливаясь через секунду. Багаж, конечно же, поставляется из очередей намного, намного быстрее этого, так что формируется целая куча из него. Большинству «служащих» не нужно ходить туда-сюда, но несколько человек ходят позади стоек регистрации, и довольно забавно наблюдать за тем, как им приходится маневрировать вокруг всё более высоких штабелей багажа, сложенных возле уже забитого конвеера.

    Ли сообщает мне, что её друг C раньше был грузчиком в авиакомпании D, но недавно его уволили. «О, должно быть, рецессия и сокращение авиаперевозок привели к уменьшению человекочасов?» - спрашиваю я. Нет, оказывается, C заявляет, что у них было полно работы, но шаткое финансовое положение авиакомпании заставило их увольнять персонал и перегружать работой оставшихся грузчиков.

    Так или иначе, мы достигаем головы очереди и я смотрю вперёд, пытаясь вступить в визуальный контакт с представителем Авиакомпании D, стоящей прямо передо мной. Она упорно смотрит на пол и громко говорит приблизительно в направлении представителя, обслуживающего соседнюю очередь «мне нужно уйти». Что она и делает, продолжая смотреть в пол. Другие представители, все занятые обслуживанием других пассажиров, не реагируют на эту новость. Возможно, они её не услышали. Или, они могли проинтерпретировать это просто как информацию – как это и было сформулировано – а не как просьбу кому-нибудь заняться покинутой очередью.

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

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

    Примерно через пять минут всего этого, путешественница позади меня вежливо трогает меня за плечо и спрашивает «Вы тоже летите в Мичиган, так ведь? Я стою в той очереди?»

    Я поворачиваюсь вполоборота к ней, вполоборота к тому сотруднику авиакомпании D, который так успешно игнорирует меня и пару дюжин людей позади меня. «Мадам,» говорю я, «посмотрите на картину в целом. Вы стоите в очереди, в конце которой некому поставить ваши чемоданы на конвеер, который не движется. Большинство грузчиков, которые должны снимать ваши чемоданы с конвеера, были уволены, и даже если бы там были грузчики, самолёт не может лететь, и вероятно его ещё даже нет в этом аэропорту. Мы с вами явно ошиблись не очередью, мы ошиблись авиакомпанией.»

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

    Хотя никак не признав того, что тут была хоть какая-то проблема, к его чести он был вежлив, выглядел достаточно компетентным в приёме моего чемодана, и добавил чемодан к башне. Когда мы уходили, я оглянулся и увидел парня обслуживающим обе очереди; эти очереди теперь продвигались с половинной скоростью, что, по моему мнению, лучше, чем ничего, хотя могу себе представить, что люди, выбравшие любую их них были менее чем в восторге. Самолёт наконец долетел, и мы в итоге получили чемодан, так что в конце концов всё сработало.

    Теперь вы знаете, почему большинство авиалиний используют «змеистую» модель W вместо модели M. Прежде всего, она предотвращает некоторые из таких проблем.

    После того всё значительно улучшилось. Примеры того, что я видел в отпуске:

    • Юпитер в противостояни
    • Ганимед
    • метеоритный дождь из Персеид
    • форель
    • лягушек
    • жаб
    • головастиков
    • черепах
    • грифов-индеек
    • ласточек
    • колибри
    • крохалей
    • гагар
    • больших голубых цапель
    • какую-то хищную птицу, которую я видел на фоне солнца, так что не смог точно разглядеть, но подозреваю, что это была скопа
    • бурундуков
    • кроликов
    • плакучую берёзу
    • львиный зев
    • ярко-зелёных стрекоз
    • овцу
    • петухов
    • фоссилизированные ракушки
    • подозрительно повреждённые деревянные байдарочные вёсла: это свидетельство активности бобровых акул. Но как они проникли зимой в мой склад с байдаркой? Неужели бобровые акулы – амфибии?
    • мою семью
    • старых друзей

    Полёт домой – где оформление багажа было устроено по модели W – прошёл без происшествий.

    Так что, вернёмся к дальнейшим невероятным приключениям в коде. Надеюсь, вам понравились мои законсервированные статьи, которые я заготовил перед отпуском.

    В следующем выпуске: еще одно дополнение про блоки итераторов.

More Posts Next page »

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker