Возможно, ошибка компилятора С# в Visual Studio 2015.

Я думаю, что это ошибка компилятора.

Следующее консольное приложение компилируется и выполняется безупречно при компиляции с помощью VS 2015:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var x = MyStruct.Empty;
        }

        public struct MyStruct
        {
            public static readonly MyStruct Empty = new MyStruct();
        }
    }
}

Но теперь все становится странно: этот код компилируется, но при выполнении выдает TypeLoadException.

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var x = MyStruct.Empty;
        }

        public struct MyStruct
        {
            public static readonly MyStruct? Empty = null;
        }
    }
}

Вы испытываете ту же проблему? Если это так, я отправлю вопрос в Microsoft.

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

У меня есть методы с разными перегрузками, например

void DoSomething(MyStruct? arg1, string arg2)

void DoSomething(string arg1, string arg2)

Вызов метода таким образом...

myInstance.DoSomething(null, "Hello world!")

... не компилируется.

Вызов

myInstance.DoSomething(default(MyStruct?), "Hello world!")

or

myInstance.DoSomething((MyStruct?)null, "Hello world!")

работает, но выглядит некрасиво. Я предпочитаю это так:

myInstance.DoSomething(MyStruct.Empty, "Hello world!")

Если я помещу переменную Empty в другой класс, все будет работать нормально:

public static class MyUtility
{
    public static readonly MyStruct? Empty = null;
}

Странное поведение, не так ли?


ОБНОВЛЕНИЕ 2016-03-29

Я открыл тикет здесь: http://github.com/dotnet/roslyn/issues/10126


ОБНОВЛЕНИЕ 2016-04-06

Здесь открыт новый тикет: https://github.com/dotnet/coreclr/issues/4049


person Peter Perot    schedule 25.03.2016    source источник
comment
Может быть воспроизведено с помощью скрипта .NET, на первый взгляд выглядит как ошибка. Примечание. Удалите свои директивы using, они не нужны для примера и излишне загромождают ваш пример кода.   -  person Heinzi    schedule 25.03.2016
comment
Лично я бы назвал это MyStruct.Null. Люди ожидают чего-то другого, говоря о пустом в контексте структур.   -  person Heinzi    schedule 25.03.2016
comment
Я думаю, что это потенциально ошибка, которую разрешено компилировать. То же самое в ошибках VS2013 при компиляции с исключением CS0523: Struct member 'ConsoleApplication1.Program.MyStruct.Empty' of type 'System.Nullable<ConsoleApplication1.Program.MyStruct>' causes a cycle in the struct layout, чего я бы наполовину ожидал   -  person Rhumborl    schedule 25.03.2016
comment
@Rhumborl: Но я думаю, что это тоже ошибка компилятора. Сообщение об ошибке было бы в порядке, если бы переменная была нестатической. Но поскольку он статичен, в макете нет цикла.   -  person Peter Perot    schedule 25.03.2016
comment
@Heinzi: Вы правы. В первом примере идеально подходит Empty, во втором больше подойдет Null. Но это не решит ошибку компилятора. ;-)   -  person Peter Perot    schedule 25.03.2016
comment
Хорошая находка. Это точно похоже на ошибку.   -  person Eric Lippert    schedule 25.03.2016
comment
Ух ты! Как часто мы сталкиваемся с вопросами, где проблема должна быть ошибкой в ​​компиляторе - это действительно так!!   -  person n8wrl    schedule 25.03.2016
comment
@PeterPerot - Хорошая находка. Как только вы опубликуете его в MS, не могли бы вы вставить сюда ссылку на выпуск? (хотелось бы, чтобы MS взялся за это)   -  person Rohit Vats    schedule 25.03.2016
comment
Я открыл тикет здесь: github.com/dotnet/roslyn/issues/10126   -  person Peter Perot    schedule 29.03.2016
comment
@ Питер Перо, это не декларация неверна. Что неправильно, так это вызов MyStruct.Empty. Из-за dot operator вызывается статическое поле типа MyStruct. Который вызывает null для типа. Позволит ли CLR вызывать null для типа? Когда вы вызываете var x = MyStruct.Empty; в методе Main, это invocation, а не просто assignment значения. Разрешит ли CLR этот вызов?   -  person Julius Depulla    schedule 30.03.2016
comment
@Peter Perot, dot operator представляет invocation. Я не понимаю, как CLR может вызывать null   -  person Julius Depulla    schedule 30.03.2016
comment
@Eric Lippert Я не думаю, что это ошибка. Это вызов нулевой ссылки. MyStruct.null буквально. Использование оператора точки представляет собой вызов, а не простое присваивание, и обращение к CLR с просьбой вызвать нулевую ссылку, тип no, приведет к исключению TypeLoadException.   -  person Julius Depulla    schedule 30.03.2016
comment
@JuliusDepulla Кид, может быть, ты захочешь взглянуть, кто такой Эрик Липперт. Если он говорит, что это ошибка, это ошибка. В любом случае ваша логика ошибочна, поскольку в ней не вызывается нулевой объект. Как бы вы сохранили нулевое значение в члене, если бы это было правдой?   -  person Corey    schedule 31.03.2016
comment
Попросите @Eric Lippert проанализировать мое объяснение, и он скажет вам, что это не ошибка, и я прав. Не нужно быть грубым   -  person Julius Depulla    schedule 31.03.2016
comment
@JuliusDepulla Ты здесь груб. Спросите Эрика, если хотите, я не собираюсь беспокоить его такими мелочами. На самом деле, если вы опубликуете свой материал как вопрос о том, как работает язык, он, скорее всего, ответит. Но имейте в виду, что он помогал писать язык C#, так что, вероятно, он понимает его гораздо лучше, чем вы.   -  person Corey    schedule 31.03.2016
comment
Сначала, когда вы посмотрите на синтаксис, вы подумаете, что это ошибка, но когда вы проанализируете, вы поймете, что это не ошибка. Я читал книги, написанные Эриком Липпертом, в настоящее время читаю Essential C# 6.0 Марка Михаэлиса и Эрика Липперта. Я знаю об Эрике и Андерсе Хейлсбергах. Но это сообщение, которое вводит в заблуждение, и я говорю, что это не ошибка.   -  person Julius Depulla    schedule 31.03.2016
comment
Во-первых, вопрос о том, является ли это ошибкой, зависит от команды C#, членом которой я больше не являюсь. Во-вторых, анализ ошибки пользователем coderealm не имеет смысла; Я подозреваю, что тамошнее кодовое царство и здесь Юлий — одно и то же лицо. Оператор точки не является оператором вызова, это оператор доступа к членам. Вызов происходит только в том случае, если доступ к члену является вызываемым свойством, вызываемым методом или вызываемым полем типа делегата. (Или несколько других подобных случаев.) В этом случае ничего из вышеперечисленного не верно; здесь нет призыва.   -  person Eric Lippert    schedule 31.03.2016
comment
@peter-perot, Кори, прочитав блог с Являются ли частные члены частью поверхности API?. Это реализация CLR, и поведение вашего кода ожидаемо. Возвращаясь к вариантам Эрика Липперта по причине исключения. Последний вариант - правильный ответ. Собственно сама программа. Это не ошибка в CLR, а желаемая реализация Microsoft.   -  person Julius Depulla    schedule 01.04.2016
comment
Одно можно сказать наверняка: мое объяснение заключается не в том, как CLR обрабатывает этот код. Но я с самого начала знал, что проблема была в коде, а не в CLR.   -  person Julius Depulla    schedule 01.04.2016
comment
О, мой Имбирный пряник-Иисус, как дела, @JuliusDepulla?! Мой код здесь не ошибка — и точка. Эта дискуссия ни к чему не приводит, так как ваши рассуждения здесь ничего нового не вносят.   -  person Peter Perot    schedule 01.04.2016
comment
@ Питер Перо Молодец. Отличная находка. Я бросил полотенце. Я лично написал письмо Джареду, и он ответил. См. его комментарии в ветке Github.   -  person Julius Depulla    schedule 01.04.2016
comment
Спасибо! :-) :-)   -  person Peter Perot    schedule 06.04.2016


Ответы (3)


Это не ошибка 2015 года, а, возможно, ошибка языка C#. Приведенное ниже обсуждение касается того, почему члены-экземпляры не могут создавать циклы и почему Nullable<T> вызывает эту ошибку, но не должен применяться к статическим членам.

Я бы представил это как ошибку языка, а не ошибку компилятора.


Компиляция этого кода в VS2013 дает следующую ошибку компиляции:

Член структуры «ConsoleApplication1.Program.MyStruct.Empty» типа «System.Nullable» вызывает цикл в макете структуры

Быстрый поиск выдает этот ответ, в котором говорится:

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

К сожалению, тип System.Nullable<T>, который используется для обнуляемых экземпляров типов значений, также является типом значений и поэтому должен иметь фиксированный размер. Заманчиво думать о MyStruct? как о ссылочном типе, но на самом деле это не так. Размер MyStruct? основан на размере MyStruct... что, по-видимому, приводит к циклу в компиляторе.

Возьмем, например:

public struct Struct1
{
    public int a;
    public int b;
    public int c;
}

public struct Struct2
{
    public Struct1? s;
}

Используя System.Runtime.InteropServices.Marshal.SizeOf(), вы обнаружите, что Struct2 имеет длину 16 байтов, что указывает на то, что Struct1? является не ссылкой, а структурой, которая на 4 байта (стандартный размер заполнения) длиннее, чем Struct1.


Что здесь не происходит

В ответ на ответ и комментарии Джулиуса Депуллы, вот что фактически происходит, когда вы получаете доступ к полю static Nullable<T>. Из этого кода:

public struct foo
{
    public static int? Empty = null;
}

public void Main()
{
    Console.WriteLine(foo.Empty == null);
}

Вот сгенерированный IL из LINQPad:

IL_0000:  ldsflda     UserQuery+foo.Empty
IL_0005:  call        System.Nullable<System.Int32>.get_HasValue
IL_000A:  ldc.i4.0    
IL_000B:  ceq         
IL_000D:  call        System.Console.WriteLine
IL_0012:  ret         

Первая инструкция получает адрес статического поля foo.Empty и помещает его в стек. Этот адрес гарантированно не равен нулю, поскольку Nullable<Int32> является структурой, а не ссылочным типом.

Затем вызывается Nullable<Int32> скрытая функция-член get_HasValue для получения значения свойства HasValue. Это не может привести к нулевой ссылке, поскольку, как упоминалось ранее, адрес поля типа значения не должен быть нулевым, независимо от значения, содержащегося в адресе.

Остальное просто сравнивает результат с 0 и отправляет результат на консоль.

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

person Corey    schedule 25.03.2016
comment
Да ты прав. Проблема обсуждается здесь: stackoverflow.com/questions/9296251/. Но поскольку он статичен, это не проблема с расположением памяти. - person Peter Perot; 25.03.2016
comment
@PeterPerot Да, поэтому в конце моего ответа я предлагаю вам отправить его как языковую ошибку. Остальная часть поста, по сути, представляет собой обсуждение того, почему это является проблемой, например, для участников... извините, если это было неясно. - person Corey; 25.03.2016
comment
+1. Я отправлю репост ошибки. Между прочим, здесь есть еще одно обсуждение: -generic-struct?forum=clr#4ceaff6a-00e4-444c-831f-796ebaeac4e4" rel="nofollow noreferrer">social.msdn.microsoft.com/Forums/vstudio/en-US/ - person Peter Perot; 25.03.2016
comment
@PeterPerot Обновлен ответ, чтобы лучше отразить это. - person Corey; 25.03.2016
comment
Этот ответ неверен, на мой взгляд. Ниже я объяснил, почему это не ошибка. - person Julius Depulla; 31.03.2016

Во-первых, при анализе этих проблем важно создать минимальный репродуктор, чтобы мы могли сузить круг проблем. В исходном коде есть три отвлекающих маневра: readonly, static и Nullable<T>. Нет необходимости воспроизводить проблему. Вот минимальная реплика:

struct N<T> {}
struct M { public N<M> E; }
class P { static void Main() { var x = default(M); } }

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

  • Исключение не вызывается использованием E. Он срабатывает при любой попытке доступа к типу M. (Как и следовало ожидать в случае исключения загрузки типа.)
  • Исключение воспроизводит, является ли поле статическим или экземпляром, доступным только для чтения или нет; это не имеет ничего общего с природой поля. (Однако это должно быть поле! Проблема не воспроизводится, если это, скажем, метод.)
  • Исключение не имеет ничего общего с «вызовом»; в минимальном воспроизведении ничего не "вызывается".
  • Исключение не имеет ничего общего с оператором доступа к членам «.». В минимальной репродукции его нет.
  • Исключение не имеет ничего общего с нулевыми значениями; ничто не может быть обнулено в минимальном воспроизведении.

Теперь давайте проведем еще несколько экспериментов. Что, если мы создадим классы N и M? Сообщу вам результаты:

  • Поведение воспроизводится только тогда, когда оба являются структурами.

Мы могли бы продолжить обсуждение вопроса о том, воспроизводится ли проблема только тогда, когда M в каком-то смысле «непосредственно» упоминает о себе, или же «косвенный» цикл также воспроизводит ошибку. (Последнее верно.) И, как отмечает Кори в своем ответе, мы также можем спросить: «Должны ли типы быть универсальными?» Нет; есть репродуктор еще более минимальный, чем этот, без дженериков.

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

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

  • Правило против структур, содержащих элементы сами по себе, здесь явно не применяется. (См. раздел 11.3.1 спецификации C# 5, который у меня есть под рукой. Я отмечаю, что этот раздел может выиграть от тщательного переписывания с учетом обобщений; некоторые формулировки здесь немного неточны.) Если E является статическим, тогда этот раздел не применяется; если он не статичен, то оба макета N<M> и M могут быть вычислены независимо.

  • Я не знаю другого правила языка C#, запрещающего такое расположение типов.

  • Возможно, спецификация CLR запрещает такое расположение типов, и CLR имеет право выдать здесь исключение.

Итак, теперь давайте суммируем возможности:

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

  • Поведение CLR правильное. Топология этого типа недопустима, и это правильно для CLR. (В этом случае может случиться так, что в CLR есть ошибка спецификации, поскольку этот факт не может быть адекватно объяснен в спецификации. Сегодня у меня нет времени заниматься спецификациями CLR.)

Предположим ради аргумента, что верно второе. Что мы можем теперь сказать о C#? Некоторые возможности:

  • Спецификация языка C# запрещает эту программу, но реализация позволяет. В реализации есть ошибка. (Я считаю этот сценарий ложным.)

  • Спецификация языка C# не запрещает эту программу, но ее можно заставить сделать это при разумных затратах на реализацию. В этом сценарии неисправна спецификация C#, она должна быть исправлена, а реализация должна быть исправлена ​​для соответствия.

  • Спецификация языка C# не запрещает программу, но обнаружение проблемы во время компиляции не может быть осуществлено по разумной цене. Это относится практически к любому сбою во время выполнения; ваша программа дала сбой во время выполнения, потому что компилятор не смог помешать вам написать ошибочную программу. Это просто еще одна программа с ошибками; к сожалению, у вас не было причин знать, что это ошибка.

Подводя итог, наши возможности:

  • В CLR есть ошибка
  • Спецификация C# содержит ошибку
  • В реализации C# есть ошибка
  • В программе есть ошибка

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


ОБНОВИТЬ:

Эта проблема отслеживается здесь:

https://github.com/dotnet/roslyn/issues/10126

Подводя итог выводам команды C# в этом выпуске:

  • Программа является законной согласно спецификациям CLI и C#.
  • Компилятор C# 6 разрешает эту программу, но некоторые реализации интерфейса командной строки вызывают исключение загрузки типа. Это ошибка в этих реализациях.
  • Команда CLR знает об ошибке, и, по-видимому, ее трудно исправить в ошибочных реализациях.
  • Команда разработчиков C# рассматривает возможность сделать юридический код выдающим предупреждение, поскольку он будет давать сбой во время выполнения в некоторых, но не во всех версиях CLI.

Этим занимаются команды C# и CLR; следить за ними. Если у вас есть какие-либо вопросы по этой проблеме, пожалуйста, напишите об ошибке отслеживания, а не здесь.

person Eric Lippert    schedule 31.03.2016
comment
Интересно. Появилась информативная статья автора @JaredPar несколько недель назад он указал, что эта конструкция является законной в C #, но незаконной в CLR, но давайте обойдемся без комментариев относительно того, правильно ли это / желательно. Достаточно сказать, что я никогда не считал приватные поля в структурах частью их контракта. - person Mike Zboray; 01.04.2016
comment
@mikez: О, ради бога, если Джаред уже на нем, то зачем мне здесь возиться? Пусть разбирается, за это ему большие деньги платят! :-) (И это то, что я получаю за то, что отстаю в чтении моего блога.) - person Eric Lippert; 01.04.2016
comment
Я считаю, что здесь есть ошибка в минимальном воспроизведении. Одно различие между вашим минимальным воспроизведением и использованием OP Nullable<T> заключается в том, что Nullable<T> также содержит поле типа T. В вашем случае вы этого не сделали, что в значительной степени сводит на нет большинство ваших аргументов в пользу того, что это законно (в отношении исходной публикации). В случае наличия поля ваша минимальная репроструктура M будет содержать структуру N<M>, которая также содержит M, что, в свою очередь, означает, что косвенно структура M содержит сама себя и, следовательно, имеет неограниченный размер. - person Lasse V. Karlsen; 01.04.2016
comment
По иронии судьбы, вы можете скомпилировать этот код с помощью компилятора Microsoft, но вы не можете запустить его с помощью среды выполнения .NET, и хотя вы не можете скомпилировать его с помощью компилятора Mono, вы можете запустите его с помощью среды выполнения Mono. (Проверено на Windows 10, .NET 3.5 и версии Mono 2.0, поставляемой с Unity 3D 5.4.0f3.) - person yoyo; 20.10.2016
comment
C++ также имеет некоторые ограничения. Страшная ошибка: поле имеет неполный тип - person Luiz Felipe; 28.12.2020
comment
Каков размер рекурсивной структуры? Конечно, это бесконечно, если только у вас нет способа остановить бета-редукцию. У тебя случайно нет настоящей машины Тьюринга? просто шучу. Как запустить на моно? он определенно делает что-то не так, возможно, он просто создал структуру нулевого размера? поэтому он может вызвать член экземпляра, я не понимаю, почему это правильное поведение. Компилятор C# тоже ошибается, потому что это ошибка. Обратите внимание, что у вас могут быть структуры с членами, не являющимися экземплярами, они не делают тип неполным в C++. Так что это также ошибка в CLR, если она не допускает статических членов. - person Luiz Felipe; 28.12.2020

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

Проблема, по-видимому, ограничена типами полей, которые являются типами значений, которые каким-то образом ссылаются на этот тип либо как общие параметры, либо как статические члены. Например:

public struct A { public static B b; }
public struct B { public static A a; }

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

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

Однако при замене A или B на class проблема исчезает:

public struct A { public static B b; }
public class B { public static A a; }

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

public struct MyStruct
{
    private static class _internal { public static MyStruct? empty = null; }
    public static MyStruct? Empty => _internal.empty;
}

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

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

person Corey    schedule 01.04.2016
comment
Классный обходной путь, @Cory. Однако, поскольку Nullable<MyStruct> является самой структурой, а не ссылочным типом, каждый вызов метода получения свойства будет создавать копию. Так что вы можете просто написать public struct MyStruct { public static MyStruct? Empty => null; }; нет необходимости создавать резервную копию null типа, допускающего значение NULL, во внутренней переменной. - person Peter Perot; 01.04.2016
comment
@PeterPerot Конечно. Я всегда забываю об эффекте копирования типов значений. Полагаю, это из-за того, что в юности я был программистом на C. - person Corey; 02.04.2016