Generics и Nullable (класс против структуры)

РЕДАКТИРОВАТЬ: ознакомьтесь с моим сообщением ниже о моем отношении к Null в C#8.0, прежде чем давать мне варианты шаблонов и тому подобное


Исходный вопрос

Я пытаюсь обновить свои базовые библиотеки, чтобы они были «нулевыми», иначе используя флаг С# 8.0 <Nullable>enable</Nullable>.

При попытке работать с абстракциями и в конкретных дженериках я столкнулся с некоторыми проблемами. Рассмотрим следующий фрагмент кода, который принимает Action и преобразует его в Func<TResult>. Это включение предварительного обнуления:

public static Func<TResult> ToFunc<TResult>(this Action action)
        => () => { action(); return default; }
;

но post-nullable-enable я, кажется, борюсь, так как я не могу сделать TResult обнуляемым (или TResult?), так как остроумие потребует ограничения либо where TResult: class, либо where TResult: struct. Я никак не могу объединить эти два ограничения, чтобы компилятор знал, что TResult может быть классом или типом значения. Что на данный момент меня раздражает - поскольку нужно уметь выражать неважно, класс или структура, она будет обнуляемой (независимо от предыдущего унаследованного дизайна .NET).

Итак, post-nullable-enable у меня есть только один вариант — дублирование кода, примеры ниже:

public static Func<TResult?> ToFuncClass<TResult>(this Action action)
        where TResult : class
        => () => { action(); return null; }
;

public static Func<TResult?> ToFuncStruct<TResult>(this Action action)
        where TResult : struct
        => () => { action(); return null; }
;

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


ОБНОВЛЕНИЕ: на самом деле, я думаю, что лучше придерживаться собственных реализаций "обработки нулей", чем использовать функцию C# 8.0, допускающую значение null. Как объект "Void" или конкретизированное решение "Option/Maybe/None", кажется, лучше передает информацию. Единственное, что меня беспокоит, это то, что это не очень помогает в продвижении языка вперед, обучении новых программистов и вводит еще один >стороннее/не родное решение для универсальной проблемы, с которой мы все имеем дело с нулевым значением. Потому что собственные реализации обработки с null великолепны и все такое, но возникают по-разному в каждой кодовой базе, должны поддерживаться сообществом, и у вас есть разные варианты. Так что было бы очень полезно и чрезвычайно полезно, если бы язык полностью реализовывал его и если бы повышался стандарт. На что я надеялся, что это будет - ясно, что это не так, и я понимаю. Но я чувствую, что это упущенная возможность.

Спасибо Ив


person Yves Schelpe    schedule 10.10.2019    source источник
comment
Нет дублирования. Типы полностью различаются в каждом случае, поэтому это не считается перегрузкой метода.   -  person Panagiotis Kanavos    schedule 10.10.2019
comment
Компилятор должен генерировать очень код для структур, допускающих значение NULL, и для ссылочных типов, допускающих значение NULL. Вот почему вы не можете выразить nullable, независимо от того, какой вкус   -  person Damien_The_Unbeliever    schedule 10.10.2019
comment
@Damien_The_Unbeliever, чтобы быть совершенно ясным, я понимаю это поведение, но довольно неуклюже видеть это в кодовой базе, где фактически вы использовали один метод для каждого случая и покончили с этим. Представьте себе это для всех общих методов, я чувствую, что это упускается из виду - даже если базовые типы (значение/ссылка) различны, поведение, которое я хочу выразить, явно одинаково в обоих случаях. Теперь при вызове метода для типа я не могу просто вызвать ToFunc, но должен буду проверить тип возвращаемого результата (здесь это даже не имеет смысла) и либо выбрать ToFuncX, либо To FuncY.   -  person Yves Schelpe    schedule 10.10.2019
comment
@YvesSchelpe это не упускается из виду, причина объяснена, и от этого никуда не деться. Во всяком случае, странно иметь метод, который намеренно возвращает null. Зачем тогда использовать нулевые ссылочные типы   -  person Panagiotis Kanavos    schedule 10.10.2019
comment
@YvesSchelpe, какую настоящую проблему вы хотите решить? Функциональные языки не возвращают null из методов, они возвращают тип unit или void. Вы можете сделать то же самое. System.Threading.Channels содержит тип VoidResult по этой причине.   -  person Panagiotis Kanavos    schedule 10.10.2019
comment
Компилятор может сделать это, если приложить достаточные усилия. C# традиционно разрабатывался таким образом, что затраты, которые вы получаете за использование функции, являются авансом (поэтому никакой магии компилятора с большими накладными расходами), но этот принцип уже давно пошел по пути додо с такими вещами, как методы итераторов. , асинхронные конечные автоматы и весь механизм замыканий и лямбда-выражений. Более уместная причина заключается в том, что это невозможно реализовать, не нарушая существующий код. Если бы не было Nullable<T> до этой функции, это можно было бы сделать, но так как это сейчас, нет костей.   -  person Jeroen Mostert    schedule 10.10.2019
comment
Вы можете использовать шаблон Null Object или Тип результата вместо возврата null. Как вы используете эти методы? Какую проблему они решают?   -  person Panagiotis Kanavos    schedule 10.10.2019
comment
@PanagiotisKanavos Я действительно использую свой собственный тип null-eqsue, потому что сам факт выражения ничего не значит. Я бы подумал, что эта функция будет хорошим решающим фактором. Тем не менее, решения сообщества все же лучше. Как объясняет Jeroen Mostert, это могло быть и все еще может быть, если бы они разрабатывали от концепции Nullable‹T›, которая ограничивает продвижение языка имхо. Потому что использование ваших собственных типов Void и т. д. вносит только большую путаницу вместо естественного принятия, когда каждый понимает, что есть что, при чтении кода. Это главная проблема для меня.   -  person Yves Schelpe    schedule 10.10.2019
comment
@YvesSchelpe, и все же вы используете null для чего-то, для чего он не предназначен.   -  person Panagiotis Kanavos    schedule 10.10.2019
comment
@PanagiotisKanavos Я использую эти шаблоны, я просто думаю, что грустно, что введение этой недоработанной функции теперь приводит к определенным ожиданиям. Может быть, им не стоило делать это в C# 8.0... Или вносить радикальные изменения, чтобы продвинуть язык вперед. Теперь это компромисс, что не очень хорошо с точки зрения удобочитаемости и появления новых людей. Поскольку они должны знать о различных реализациях шаблонов Null Object. Я бы предпочел, чтобы ее не было, чем эта полусырая функция, поскольку я чувствую, что теперь она только добавляет путаницы.   -  person Yves Schelpe    schedule 10.10.2019
comment
@PanagiotisKanavos и все же вы используете null для чего-то, для чего он не предназначен, объясните, пожалуйста? Как вы можете заключить это из моих примеров?   -  person Yves Schelpe    schedule 10.10.2019


Ответы (1)


Причина объясняется в разделе Попробуйте ссылочные типы, допускающие значение NULL в раздел The issue with T?. По сути, нет никакого способа обойти это.

Во-первых, что такое T, когда мы используем T?? Является ли он обнуляемым или необнуляемым?

Естественное определение T? означало бы любой тип, допускающий значение NULL. Однако это означало бы, что T будет означать любой ненулевой тип, а это неверно! Сегодня можно заменить T типом значения, допускающим значение NULL (например, bool?).

Во-вторых, типы, используемые в каждом случае, различаются: string? по-прежнему является string, а int? - это Nullable<int>. Сгенерированные конкретные методы в каждом случае совершенно разные. В одном случае вы получаете

Func<string> ToFuncClass<string>(this Action action)

В другом вы получаете

Func<Nullable<int>> ToFuncStruct<int>(this Action)

Далее важно отметить, что ссылочный тип, допускающий значение NULL, — это не то же самое, что тип значения, допускающий значение NULL. Типы значений, допускающие значение NULL, сопоставляются с конкретным типом класса в .NET. Итак, инт? на самом деле можно обнулить. Но для string? это фактически та же строка, но с атрибутом, сгенерированным компилятором, который аннотирует ее. Это сделано для обратной совместимости. Другими словами, строка? является своего рода поддельным типом, тогда как int? не является.

Пример статьи демонстрирует это:

Это различие между типами значений, допускающими значение NULL, и ссылочными типами, допускающими значение NULL, проявляется в следующем шаблоне:

void M<T>(T? t) where T: notnull

Это будет означать, что параметр является версией T, допускающей значение NULL, и T ограничено значением notnull.

Если бы T была строкой, то фактическая подпись M была бы:

M<string>([NullableAttribute] T t)

но если бы T было int, то M было бы

M<int>(Nullable<int> t)

Эти две подписи принципиально различны, и эта разница несовместима.

person Panagiotis Kanavos    schedule 10.10.2019
comment
Спасибо за четкое объяснение, но разве это не доказывает, что они не должны были вводить nullable. Это сделано для обратной совместимости подразумевает, что мы застряли с функцией, которая могла бы быть очень полезной для выражения вашего кода и удобочитаемости вашего кода в настоящее время по существу дублирование кода - только для факта обратной совместимости . Тогда я предпочел бы, чтобы системы были переработаны для этого. Боюсь, я буду придерживаться своих старых нулевых шаблонов, так как я просто не могу использовать это в данный момент. Я понимаю семантическую разницу и необходимость в ней, если нужна обратная совместимость, но боюсь, что тогда это бесполезно. - person Yves Schelpe; 10.10.2019
comment
@YvesSchelpe нет, обратная совместимость не затронута, и преимущество без дублирования кода огромно. Я добавил NRT в старые проекты, и мне не пришлось ничего дублировать. Однако методы ToFunc определенно странные — они возвращают фиктивные значения и повторно вводят ту самую проблему, которую NRT должны решить. Что вы пытаетесь решить этим? Если вы хотите преобразовать действия в функции, почему бы не использовать один из распространенных шаблонов, например возврат нулевой объект или создать свой собственный объект Void? - person Panagiotis Kanavos; 10.10.2019
comment
как я уже сказал, я бы к тому, я хотел упростить пример. общие шаблоны могут быть таковыми для вас, но 90% людей, с которыми я программирую, не знают о них, поэтому наличие его на правильно поддерживаемом языке было бы лучше, чем эта функция сейчас, когда вам все еще нужно полагаться на свои собственные (или унаследованные) реализации работы с null (например, шаблон Null Object или объект Void и многие другие фактические решения). Но я понимаю, что сейчас это никак не обойти, так как они это внедрили. Я просто не вижу в этом ценности, уж точно не для продвижения языка вперед. - person Yves Schelpe; 10.10.2019
comment
@YvesSchelpe У меня точно такая же проблема с использованием C # 7. Мои подписи public static T GetOrCreate<T>(ref T value, object syncRoot, Func<T> factory) where T : class и public static T GetOrCreate<T>(ref T? value, object syncRoot, Func<T> factory) where T : struct. Оба метода содержат около 7 строк кода, полностью идентичных друг другу. Это C#7, и я надеялся, что C#8 даст мне возможность решить эту проблему. - person Sebastian Schumann; 10.10.2019
comment
@SebastianSchumann действительно - у меня были разные надежды на эту функцию. Для меня это не решает ничего, что я не мог сделать раньше с реализацией, которая у меня есть с использованием шаблона Option. Тем не менее, это вводит еще один уровень абстракции или надежности, который вам необходимо поддерживать - и объяснять новичкам в вашей кодовой базе, хотя я чувствую, что язык справится с этим. Может со временем... - person Yves Schelpe; 10.10.2019
comment
@YvesSchelpe: это решает большую проблему: NullReferenceException, потому что многие программисты не проверяют наличие нулей, просто не ожидают, что они появятся в параметрах. Это основная причина нулевых ссылок. Во-вторых, вам не нужно два имени, достаточно одного. Дублирование кода, конечно, но это не такая уж большая цена (учитывая, что эти два типа каким-то образом не связаны). Я бы хотел, чтобы Nullable<T> стал конструкцией языка/CIL, которая принимала бы и классы, и структуры, но на самом деле для чего-то подобного еще слишком рано. - person firda; 10.10.2019
comment
@firda, к сожалению, необходимы два имени, потому что сигнатуры двух функций идентичны, за исключением типа возвращаемого значения. - person Panagiotis Kanavos; 10.10.2019
comment
@PanagiotisKanavos Как у Себастьяна могло быть две перегрузки GetOrCreate? Как раз собирался проверить это, когда увидел его комментарий... и просто поверил, что это работает. - person firda; 10.10.2019
comment
@firda Мои перегрузки содержат T в качестве параметра. Параметры включаются в сигнатуры методов. Yves T содержится только в возвращаемом типе. Тип возвращаемого значения не принадлежит сигнатуре метода. Вот почему ему нужны два разных имени метода. - person Sebastian Schumann; 10.10.2019
comment
@firda Я согласен, что программисты должны знать о null. Я просто хотел бы, чтобы они сделали это правильно - это только добавит путаницы (не забывайте, что вы можете подавить множество ошибок/предупреждений, и многие из них вводят пункт типа #nullable enable внутри кодовых баз). Это не шаг вперед. Я бы предпочел, чтобы они приняли стиль программирования, такой как шаблон Option, поэтому разработчики API вынуждены выражать намерения, а потребители вынуждены иметь дело с чем-то или ничего. Я гарантирую, что это не решит проблему, а только усложнит ситуацию. Так что буду придерживаться своего варианта... - person Yves Schelpe; 10.10.2019
comment
@YvesSchelpe: Если бы только в C# было что-то вроде специализации шаблонов в C++. Это также могло бы решить эту проблему (Nullable<T> where T: class - это простая ссылка без встроенного логического значения, используемого для структур), но это C#, и они должны были сделать его обратно совместимым... Я согласен, что это выглядит полумерой, несовершенной, но все же хорошо. С моей точки зрения, это улучшение, хорошая попытка. Посмотрим, как его примут. - person firda; 10.10.2019
comment
@firda и YvesScelpe, это действительно не чат-сайт. - person Panagiotis Kanavos; 10.10.2019
comment
@PanagiotisKanavos Я согласен, но дело в том, что эта функция потенциально более вредна, чем при решении ее проблем. И я считаю, что люди должны об этом знать. Если 75% официальной документации посвящено стратегиям выхода (нулевой прощающий оператор, бесчисленные директивы и т. д.) - я думаю, об этом следует знать. Так что извините за обсуждение. На всякий случай, ваш ответ правильный, увы, мне не нравится философия, но технически это то, что есть сейчас. Грустно. - person Yves Schelpe; 10.10.2019
comment
Связанная статья делает (для меня) неочевидное утверждение, что: Естественное определение T? означало бы любой тип, допускающий значение NULL. Однако это означало бы, что T будет означать любой тип, не допускающий значения NULL, а это неверно! — потому что, естественно, я бы отверг это понятие; Т? должен был быть обнуляемым, независимо от того, является ли T или нет. Тот факт, что эти два метода несовместимы, не имеет значения, потому что ни M‹SomeClass›(), ни M‹SomeOtherClass›() — и когда дело доходит до структур, во время выполнения каждый экземпляр универсального типа все равно отдельно JIT-комбинируется. - person Eamon Nerbonne; 28.05.2021