Подтвердить что ты не робот

Возможно, ошибка компилятора С# в 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!")

или

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

4b9b3361

Ответ 1

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

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


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

Элемент Struct '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 и отправляет результат на консоль.

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

Ответ 2

Прежде всего, важно анализировать эти проблемы, чтобы создать минимальный репроектор, чтобы мы могли сузиться там, где проблема. В исходном коде есть три красные сельди: 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. (Как и следовало ожидать в случае исключения нагрузки типа.)
  • Исключение воспроизводит, является ли поле статическим или экземпляром, только для чтения или нет; это не имеет ничего общего с природой поля. (Однако это должно быть поле! Проблема не воспроизводится, если это, скажем, метод.)
  • Исключение не имеет ничего общего с "вызовом"; ничто не "вызывается" в минимальном воспроизведении.
  • Исключение не имеет ничего общего с оператором доступа к члену ".". Он не отображается в минимальном воспроизведении.
  • Исключение не имеет ничего общего с nullables; ничто не обнуляется в минимальном воспроизведении.

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

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

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

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

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

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

  • Я не знаю другого правила на языке С#, которое запрещало бы эту компоновку типов.

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

Итак, теперь подведем итоги:

  • У CLR есть ошибка. Топология этого типа должна быть законной, и это не так, чтобы CLR выбрасывать здесь.

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

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

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

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

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

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

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

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


ОБНОВЛЕНИЕ:

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

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

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

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

Команды С# и CLR находятся на этом; следить за ними. Если у вас есть какие-либо проблемы с этой проблемой, пожалуйста, отправьте сообщение на вопрос отслеживания, а не здесь.

Ответ 3

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

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

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

Ух, теперь я чувствую себя грязным. Bad OOP, но он демонстрирует, что проблема существует, не вызвав вообще никаких генериков.

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

Однако, изменяя либо 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, поэтому я бы не использовал его для критически важного кода, но в качестве обходного пути он по крайней мере позволяет вам выполняйте эту работу до тех пор, пока не появится соответствующее решение.

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