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

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

Возьму себе на заметку для следующей статьи. :)

Да! Хочу единорожку-деппа!

Когда в следующий раз будем куда-нибудь погружаться, попросим nookino помочь в этом вопросе. :)

О, сколько нам открытий чудных…
Интересно, нет ли похожих закавык и с ссылочными параметрами — вдруг некоторые из них не обязательно инициализировать перед передачей методу?
Не только ссылочные параметры. Там вообще полная вакханалия. Можно не инициализировать поля в конструкторах структур. Можно не инициализировать локальные переменные структурных типов, перед их использованием.
Дичь
struct Empty 
{
    //int gameChanger;
}

struct DeepEmpty
{
    public Empty field;

    public DeepEmpty(int arg)
    {
        //field = new Empty();
    }
}
class Program
{
    static void RefTest (ref DeepEmpty arg) 
    {
        Console.WriteLine(arg);
    }
    static void Main(string[] args)
    {
        DeepEmpty empty;

        //empty = new DeepEmpty();

        RefTest(ref empty);

        Console.ReadKey();
        return;
    }
}

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

не вызывая конструкторы на каждый чих, а просто определив все поля

Не понял, что подразумевается под определением полей?
Пример выше компилируется, только до тех пор, пока тип Empty является пустой структурой. Если раскомментировать поле gameChanger, то придётся раскомментировать и инициализацию поля field в пользовательском конструкторе, и инициализацию локальной переменной empty в методе Main. На мой взгляд, ситуация аналогична описанной в статье.
В принципе, логично, что для пустых структур сделано исключение, и их не требуется инициализировать, не только в случае с out параметрами, но и во всех остальных случаях, когда C# требует явной инициализации. И было бы странно, если бы это касалось только out параметров. Но это становится очевидным, только если вы уже знаете о том, что для пустых структур существует некое исключение из правила. А информация эта не очень широко известная, в частности из-за того, что она не упоминается там, где её логично было бы упомянуть, например здесь.
Не понял, что подразумевается под определением полей?

Вот это:


DeepEmpty empty;
empty.field.gameChanger = ...;

вместо явных вызовов конструкторов.

Жаль, что раньше этот комментарий не увидел и написал ниже то же. Есть ли логическое объяснение этой дичи? Зачем делать исключение для пустых структур, особенно если учесть, что пустая структура бессмысленна сама по себе?

Есть, выше же ссылку кидали. Если вам лень искать и читать то вот краткая выдержка

So what's the bug?

The bug that you have discovered is: as a cost savings, the C# compiler does not load the metadata for private fields of structs that are in referenced libraries. That metadata can be huge, and it would slow down the compiler for very little win to load it all into memory every time.

And now you should be able to deduce the cause of the bug you've found. When the compiler checks to see if the out parameter is definitely assigned, it compares the number of known fields to the number of fields that were definite initialized and in your case it only knows about the zero public fields because the private field metadata was not loaded. The compiler concludes «zero fields required, zero fields initialized, we're good.»

Like I said, this bug has been around for more than a decade and people like you occasionally rediscover it and report it. It's harmless, and it is unlikely to be fixed because fixing it is of almost zero benefit but a large performance cost.

Ну, "large performance cost" — это был аргумент для старого компилятора. Roslyn, как видно из статьи, всё равно выдаёт предупреждение, и от замены его на ошибку производительность не ухудшится.


Судя по коду, основной аргумент за такое поведение сейчас — совместимость со старым компилятором :-(

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

Спасибо. Я только вчера завел тут аккаунт. Не привык еще.

Константы никак не относятся к экземпляру структуры. Так что они тут не причём.

Отличный вопрос для собеседования! (нет)

Да-да, мы с Andrey2008 тоже отшучивались на эту тему. :)

Поздно, HRы уже дописали его в свой «Вопросы к компьютерщику.docx».

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

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

А кто сказал что мне нужен супер-мачурный специалист? Мб просто читающий хабр миддл который интересуется кишочками подойдет?

Опять же, миддлов много, но среди них вы отдадите предпочтение тем, кто читает хабр, и то не всем, а только тем, кто заметил данную конкретную статью.
А кроме хабра, кстати, есть множество интересных изданий. У меня есть гипотеза, почему Хабр умирает: русские программисты наконец-то начали учить английский, поэтому формат русских сайтов их перестал интересовать. А Хабр — это такой ламповый ресурс, куда иногда приятно прийти, почитать статьи типа этой. Редко, но бывает.
Опять же, миддлов много, но среди них вы отдадите предпочтение тем, кто читает хабр, и то не всем, а только тем, кто заметил данную конкретную статью.

Такое ощущение, что это единственный вопрос.


По этой причине вообще ничего по нюансам спрашивать нельзя, ведь все эти нюансы описаны только в одном-двух %blogname%


Я же не предлагаю делать далекоидущие выводы, особенно если человек не знает. Не знает — и ладно. Знает — прекрасно


У меня есть гипотеза, почему Хабр умирает: русские программисты наконец-то начали учить английский, поэтому формат русских сайтов их перестал интересовать. А Хабр — это такой ламповый ресурс, куда иногда приятно прийти, почитать статьи типа этой. Редко, но бывает.

Альтернатив хабру я не знаю. Медиум — довольно помоечный и без комментов, реддит — там кто в лес, кто по дрова, в целом уже ближе, но все ещё не то. Уровень контента не очень выдержан. ycombinator/hackernews тоже странный формат имеют.


Если у вас есть примеры годных изданий — поделитесь, пожалуйста

Что ж, раз официальная документация и спецификация языка ответов нам не дали

Ну так ноги растут из того, каким образом компилятор C# работает со структурами из отдельных сборок.
Липпет говорит о том, что подобное поведение было известно еще лет 10 назад.
тут: stackoverflow.com/a/58633459
и Тут: docs.microsoft.com/en-us/archive/blogs/ericlippert/a-definite-assignment-anomaly

Спасибо за ссылки, интересно.


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


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


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


P.S. Мне, кстати, в последнее время очень достовляет полазить в коде некоторых продуктов от Microsoft. Roslyn, MSBuild, CoreCLR, например. Не то, чтобы я по вечерам читал исходники, но если нужно что-то раскопать — с удовольствием. Так что в некотором будущем ещё чем-нибудь порадую, надеюсь. :)

момент этот неочевиден

На мой взгляд, с пустыми структурами, логику понять можно — для них нет других вариантов кроме default(MyEmptyStruct).
А вот с другими сборками — да это довольно интересное поведение, которое трудно хоть как-то объяснить:)
Цитируя документацию, вы выделили жирным немного не тот кусок. Обратите внимание на
do not have to be initialized before being passed in a method call

Естественно что буден некорректным ваш вывод про
out-параметры должны быть проинициализированы

Правильно, out параметры не должны быть инициализированы до вызова метода, но они должны быть инициализированы в теле метода.


public bool GetRegripPosition(MeshCalculationContext context, 
            ILinearAxis axis, Position axisOffset, 
            [NotNullWhen(returnValue:true)] out Position regripPosition)
{
            if (ВСЕ ХОРОШО)
            {
                   regripPosition = ЧТО-ТО ОСМЫСЛЕННОЕ;
                   return true;
            }

            regripPosition = default!; // ЭТО ВООБЩЕ НЕ ИМЕЕТ СМЫСЛА И ВРЕМЕНАМИ БЕСИТ, НО СТАНДАРТ ТРЕБУЕТ
            return false;
}
но они должны быть инициализированы в теле метода.

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

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


DistortNeo, отвечу тут, так как у меня коментарии все еще на премодерации.
[NotNullWhen] не более, чем атрибут, который указывает синтаксическому анализатору о том, что он должен игнорировать проверки на null, если метод возвращает false. Out параметр приходится инициализировать в любом случае. Если такое поведение не устраивает, то можно использовать ref параметр, который не требует обязательной инициализации в теле метода. В стандарте с логикой все в порядке и мое "бесит" — очень субъективно. :)


Просто исторически так сложилось, что код вроде


if (int.TryParse("99", out int num))
{
   .....
}

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

// ЭТО ВООБЩЕ НЕ ИМЕЕТ СМЫСЛА И ВРЕМЕНАМИ БЕСИТ, НО СТАНДАРТ ТРЕБУЕТ

Можно попробовать внести изменение в стандарт. Чтоб вместо


[NotNullWhen(returnValue:true)]

можно было писать


[MayBeNotInitializedWhen(returnValue:false)]

Соответственно, вызывающий код не имел бы права использовать переменную, не проверив результат функции.

Я думаю, возникло некоторое недопонимание.


Вы привели фрагмент из документации касаемо вызывающего метода, который использует аргументы для соответствующих out-параметров. Про него говорится следующее:


Variables passed as out arguments do not have to be initialized before being passed in a method call.

Я же приводил фрагмент касаемо вызываемого метода и требований к его out-параметрам. Собственно, этот же кейс и рассматривается в статье. Про него:


However, the called method is required to assign a value before the method returns.

Небольшое дополнение, чисто поржать...


        private static void CheckYourself(out ITestInterface obj)
        {
        }
        private interface ITestInterface
        {
        }

Так нельзя!!!
WeatherForecast.cs(21, 29): [CS0177] The out parameter 'obj' must be assigned to before control leaves the current method


        private static void CheckYourself(out MyStruct obj)
        {
        }

        private struct MyStruct : ITestInterface
        {
        }

        private interface ITestInterface
        {
        }

А вот так — нормально!!! :)

А что не так-то? В первом случае out-параметром может быть экземпляр любого типа, реализующего ITestInterface, либо null. Во втором случае — вполне конкретный тип.

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


// [38 13 - 38 27]
    IL_0001: ldarg.0      // obj
    IL_0002: initobj      WebUiTranslate.WeatherForecast/MyStruct

Это можно рассматривать как оптимизацию. Ок, тогда пусть он оптимизирует инициализацию вроде obj = default;, но все же требует ее присутсвие в исходном файле.


А то сейчас и это компилируется, когда структура пустая:


        MyStruct test;
        CheckYourself(ref test);
        private struct MyStruct 
        {
            //public int a;
        }
         private static void CheckYourself(ref MyStruct obj)
        {
            //obj = default;
        }

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


Первый — инициализация нулями:


MyStruct test = new MyStruct();

То же самое:


MyStruct test = default;

Второй — через определение значений полей:


MyStruct test;
test.Field1 = ...;
test.Field2 = ...;

При этом во втором случае предварительная инициализация нулями не требуется. Соответственно, если в структуре нет полей, то она автоматически считается проинициализированной.


Ок, тогда пусть он оптимизирует инициализацию вроде obj = default;, но все же требует её присутствие в исходном файле.

Тогда это вступит в противоречие со вторым способом инициализации.

Почему же только два способа? Например, ниже третий и четвертый одновременно. :)


        MyStruct test = new MyStruct(1, 2) {a = 3, b = 4};
        CheckYourself(ref test);

        private struct MyStruct 
        {
            public MyStruct(int ia, int ib)
            {
                a = ia;
                b = ib;
            }
            public int a;
            public int b;
        }

В любом случае мы должны должны инициализировать все поля структуры. Либо через конструктор, либо явно, либо через список инициализации. И понятно, что если нечего инициализировать, то можно считать структуру проинициализированной. Тут я протормозил, спасибо за объяснение. Но все же я остаюсь при своем мнении в плане того, что это решение неправильное, так как приводит к различному поведению компилятора в случае пустых структур и тех, которые содержат поля. Нет никакого противоречия требовать явной инициализации пустой структуры по default или new MyStruct(), так как в этом случае добавление поля в структуру гарантированно не вызовет никаких изменений в вызывающем коде.


А сейчас это не так. Впрочем, это не особо критическая проблема. Но пообщаться было интересно.

Почему же только два способа? Например, ниже третий и четвертый одновременно. :)

Потому что их реально только два. Всё остальное — синтаксический сахар.


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


Второй способ: инициализация полей.


Вызов конструктора для структур эквивалентен вызову магического статического метода с первым out-параметром. Вместо


var myStruct = new MyStruct(args...);

можно писать


MyStruct.Init(out var myStruct, args...);

IL-код будет идентичным с точностью до сигнатуры вызываемого метода. Ну а инициализация этого out-параметра в Init-методе сводится к одному из вышеперечисленных способов. Можете написать в нём myStruct = default, тогда будет сделан initobj, а можете просто определить поля.


Либо через конструктор, либо явно, либо через список инициализации.

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


То есть вот эта строчка:


MyStruct test = new MyStruct(1, 2) { a = 3, b = 4 };

эквивалентна:


MyStruct test = new MyStruct(1, 2);
// Здесь всё поля test полностью определены
test.a = 3;
test.b = 4;

Нет никакого противоречия требовать явной инициализации пустой структуры по default или new MyStruct()

Противоречие есть, потому что стоит добавить поле — и подобная явная инициализация больше не нужна.


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


SomeStructType someStruct;
someStruct.States.State1 = value1;
someStruct.States.State2 = value2;

Следуя вашей логике, для пустой структуры мне пришлось бы её отдельно инициализировать:


SomeStructType someStruct;
someStruct.States = default;

То есть закономерность нарушается.

Я, возможно, неправильно понимаю слово "закономерность". Но вроде бы это должно означать, что пустая структура инициализируется так же, как и не пустая. Да, пустую структуру было бы неплохо инициализировать. В ином случае посмотрите пример ниже, где в NET 5 структура CancellationToken внезапно начала требовать инициализации. Причина — раньше ее не нужно было явно инициализировать.


Еще раз повторюсь: явная инициализация пустой структуры не мешает, вы ее всегда можете переписать при введении новых полей. Если кода инициализации нет вообще — его нужно будет написать. И в этом разница, когда приходится расширять структуру с 0 полей до N, или с N до N+X.

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

Вот только переменная типа MyStruct имеет размер 0 байт, а ITestInterface — 4/8 байт.


Соответственно, в первую переменную можно положить только одно значение (default), а во вторую — дофига разных (как минимум null и default(MyStruct), причём упакованных структур может быть сколько с разными адресами).


Соответственно, нет никакого смысла спрашивать программиста что он положит в out MyStruct obj — но важно спросить что он положит в out ITestInterface obj.

Вот только переменная типа MyStruct имеет размер 0 байт

Вообще-то 1 байт.

Это "приколы" выравнивания, значимых байт в ней ноль.

Когда мы работаем с ref и out в метод передается указатель (адрес). Так что с 0 байтов в любом случае мимо. Да, во втором случае имплементацией интерфейса могут быть структуры, классы. А так как объект может поддерживать много интерфейсов, то без явной инициализации не обойтись. Претензии есть к первому варианту, так как при изменении MyStruct смысл что-то спрашивать у программиста внезапно появляется.

Проверил этот код не только на NET 5, но и на Mono 6.8.0.105. Поведение ref и out абсолютно идентичное: пустая структура не требует инициализации. Я не знаю чем это объяснить, готов поверить в мировой заговор рептилоидов. :)

>> Особо прошу обратить внимание на выделенное предложение.
Которое на английском…

А что не так? Оно вроде простое.
Гласит, что вызываемый метод должен записать в out-параметр значение до того, как вернет управление.

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

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

Отличный разбор, понравилось, спасибо.


Ещё один 547й повод не использовать out/ref параметры никогда.


Кстати, бонус: в .net5.0 баг не воспроизводится и ошибка компиляции показывается верно


img
img

Поставил C# 2.0 — поведение не поменялось. Можете сами проверить:


1.


<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.1</TargetFramework>
        <LangVersion>2</LangVersion>
    </PropertyGroup>
</Project>

2.


<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <LangVersion>2</LangVersion>
    </PropertyGroup>
</Project>

Код


using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }

    static void Foo(out CancellationToken token)
    {

    }
}

Нет смысла менять версию C# в проекте, там проблема в библиотечном коде.


Определение в NET 4.8


public struct CancellationToken
    {
        // The backing TokenSource.  
        // if null, it implicitly represents the same thing as new CancellationToken(false).
        // When required, it will be instantiated to reflect this.
        private CancellationTokenSource m_source;
        //!! warning. If more fields are added, the assumptions in CreateLinkedToken may no longer be valid
..................
 }

Теперь в NET 5.0


  public readonly struct CancellationToken
  {
    private readonly 
    #nullable disable
    CancellationTokenSource _source;
.............................
}

readonly структура требует readonly полей, включая приватные. Эти поля мы можем инициализировать только в конструкторе. Определить конструктор по умолчанию мы не имеем возможности, так как это структура. В итоге логика с нулем публичных полей больше не работает — мы приплыли! :)


Воспроизводим поведение CancellationToken из NET 5


        MyStruct test; 
        CheckYourself(ref test);

        public readonly struct MyStruct
        {
            private readonly object _test;

            public MyStruct(object obj)
            {
                _test = obj;
            }
        }

Не компилируется, требует инициализации.

Спасибо, действительно! Получается, что проблема в компиляторе в целом осталось, но проблема конкретно с CancellationToken'ом за нас решена таким вот образом.


Ну, на безрыбье и рак — ридонли структура.


Правда объяснение немного некорректное получается


readonly структура требует readonly полей, включая приватные. Эти поля мы можем инициализировать только в конструкторе. Определить конструктор по умолчанию мы не имеем возможности, так как это структура. В итоге логика с нулем публичных полей больше не работает — мы приплыли! :)

У нас как было 0 публичных полей — так и осталось.


Воспроизводим поведение CancellationToken из NET 5

Напоминаю, что тип должен быть в другой сборке, иначе не работает ленивость компилятора :)

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


        public Task StopAsync(CancellationToken cancellationToken)
        {
            CancellationToken test;
            Test(ref test);

            Test2(out CancellationToken test2);

            return Task.CompletedTask;
        }

        private void Test(ref CancellationToken token)
        {

        }

        private void Test2(out CancellationToken token)
        {

        }

$ dotnet --version
5.0.103


Теперь я создаю во внешней сборке


    public struct MyStruct
    {
        private int _test1;
    }

и вызов


Test3(out MyStruct test3);
private void Test3(out MyStruct test)
        {

        }

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


  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
    <Nullable>enable</Nullable>

Все, получилось!


  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>9.0</LangVersion>
  </PropertyGroup>

Теперь оба не компилируются


        private void Test2(out CancellationToken token)
        {

        }

        private void Test3(out MyStruct test)
        {

        }

Честно говоря, теперь у меня нет логического объяснения, как все это работает.

Попробуйте в MyStruct поменять тип поля на String, например. Или на тот же CancellationTokenSource — должно начать компилироваться c предупреждением. У меня начало, по крайней мере. Кстати, VS сразу начала подсвечивать это место, и warning появился.


image

Сделал поле не value типа


    public struct MyStruct
    {
        private string _test1;
    }

Поведение такое же, как в случае с int.


Моя структура проекта (стрелками показаны зависимости):
Главный модуль -> библиотека 1 -> библиотека 2


MyStruct объявлен в "библиотека 2", вызывающий код в "библиотека 1"


Проблема в том, что при схеме
net5.0 -> netstandard2.1 -> netstandard2.1
все компилируется. И CancellationToken без явной инициализации, и оба варианта MyStruct (string и int)


При схеме net5.0 -> net5.0 -> netstandard2.1
CancellationToken не компилируется. MyStruct c с полем типа int не компилируется, замена int на string приводит к успешной компиляции.


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


P.S. Спасибо за статью. Очень интересная тема :)

Для .NET 5, в общем, нужная новая статья — сиквел...


P.S. Спасибо за статью. Очень интересная тема :)

Рад, что понравилась.
В будущем надеюсь ещё чем-нибудь порадовать. :)

Пусть меня опять заминусят, но мне этот цирк не нужен: я просто пропишу в coding conventions требование обязательной явной инициализации структур.

С чего я и начал: просто не используйте ref/out и будет счастье. Чем помнить нюансы разминирования с одной рукой связанной за спиной лучше не пользоваться дорогой с минами

Посмотрел, кстати, вербозные логи сборки.
Для .NET 5 проектов компилятор запускается с /warn:5, для .NET Core 3.1 — с /warn:4. Отсюда разница в предупреждениях.

Спасибо за оценку! :)


Да, с .NET 5 интересный момент. В его случае на "пустые структуры" уже начинает выдаваться warning и VS даже сразу подсветочку организует, а пример с CancellationToken действительно не компилируется. По крайней мере, если собирать через dotnet build или из Visual Studio.


А вот если напрямки позвать компилятор, то поведение остаётся таким же (по крайней мере, в моих экспериментах, которые я сейчас наспех сделал). Возможно, при использовании сборочной системы при сборке .NET 5 проектов как-то иначе компилятор зовётся или что-нибудь в этом духе.


Но это всего лишь предположение. Чтобы поточнее сказать, надо бы уже на уровень выше подняться, потрошить MSBuild, CscTask, и с его этого уровня постепенно опускаться, исследовать.

Посмотрел по поводу CancellationToken для .NET Core 3.1 проектов и .NET 5 проектов. Один solution, 2 проекта (под соответствующие фреймворки). Код один и тот же, вынесен в общий файл. Сам фрагмент кода классический, который мы рассматривали в статье:


public void CheckYourself(out CancellationToken ct)
{ }

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


Для .NET Core 3.1 ожидаемо компилируется.
Для .NET 5 CancellationToken перестаёт считаться пустой структурой.


При анализе полей типа CancellationToken находится поле, для которого GetActualField возвращает значение не null. Для этого же поля ShouldIgnoreStructField возвращает false из-за значения подвыражения IsIgnorableType(memberType).


Как следствие, мы получаем non-null значение переменной field, и потом проверяем тип полученного поля. Тип — не пустая структура -> возвращаем false, считаем CancellationToken не пустой структурой -> создаём слот -> проводим анализ, получаем ошибку -> кол-во ошибок strict и compat анализа одинаковое, выдаём их.


Что за поле такое, спросите?


А вот, что:

image


Выглядит странно, да? Смотрим, откуда же наш такой интересный CancellationToken подтянулся: System.Runtime.dll.


Пути до этой библиотеки явно указываются в строке вызова компилятора (смотрел вербозные логи MSBuild) и отличаются для .NET Core 3.1 и .NET 5.


А теперь, внимание. Ниже 2 скриншота ildasm для CancellationToken из разных библиотек разных фреймворков.


.NET 5

image


.NET Core 3.1

image


Заметили разницу? В случае с .NET Core 3.1 нет _dummyPrimitive.
Из-за этого один CancellationToken — пустая структура, а другой — нет. Как следствие, в одном случае — компилируется, в другом — нет.


P.S. Если запустить приложение на .NET 5, использущее CancellationToken, взять у него тип и спросить сборку, можно заметить, что тип вообще из другой тянется.

тезисы, взятые с docs.microsoft.com

Кажется я нашел пункт который говорит, что Empty структуры в Out параметрах можно не инициализировать в теле функции:
Тут
Definite assignment

A struct_type variable is considered definitely assigned if each of its instance variables is considered definitely assigned.

Получается, что такое поведение структур описано в стандарте, как минимум в драфте версии 6.0
https://github.com/ljw1004/csharpspec/blob/gh-pages/variables.md


In order to determine that each used variable is definitely assigned, the compiler must use a process that is equivalent to the one described in this section.

The compiler processes the body of each function member that has one or more initially unassigned variables. For each initially unassigned variable v, the compiler determines a definite assignment state for v at each of the following points in the function member.

Процесс сложный, в связи с этим наблюдаются забавные расхождения в поведении компилятора, когда компилируются две сборки с CancellationToken под net5.0 и netstandard2.1.

Хм, интересно.


Ачивмент ваш. :)

image


Могли бы при описании out параметров в спецификации сразу дать ссылочку и на этот раздел что-ли. Вот здесь, например:


Every output parameter of a method must be definitely assigned before the method returns.
Все поля структур должны быть проинициализированы. А если полей нет, зачем требовать?
Имхо, довольно логично добавить код, проверяющий, что «пустая» структура не должна требовать инициализации. Например, такая:

public struct MyStruct { public string _test => "Test"; }

static void CheckYourself(out MyStruct obj) { } // no compilation error

Зарегистрируйтесь на Хабре, чтобы оставить комментарий