Comments 34
Все-таки аббревиатура "DU" не настолько очевидна. Гугл ИИ с моими контекстными подсказками догадался, что это "Discriminated Unions" (а затем прочитал тег в конце), но логично было бы просто упомянуть это в начале статьи.
Проблема заключается в том, что поддержка объединений будет только начиная с версии на которой они вышли. Это потенциально отрезает большую часть аудитории, например движка Unity (который до сих пор на .netstandart 2.1) или многих библиотек, которые поддерживают старые рантаймы.
Это не так. Это фича языка, а не рантайма. Вот прямо сейчас я проверил, на netstandard2.1 билдится при установленном .NET SDK 11 Preview.
#:property TargetFramework=netstandard2.1
#:property PublishAot=false
#:property LangVersion=preview
Pet pet = new Cat("Whiskers");
Console.WriteLine(pet switch
{
Cat c => $"Cat: {c.Name}",
Dog d => $"Dog: {d.Name}",
});
public record class Cat(string Name);
public record class Dog(string Name);
public union Pet(Cat, Dog);
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public sealed class IsExternalInit : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
Спасибо за уточнение! Статью обновил, добавив подробности про обратную совместимость.
Однако стоит разделять понятия: хотя код и собирается под .NET Standard 2.1, это всё равно требует от разработчика наличия нового SDK. В том же юнити его меняют не так часто.
Кроме того, остается вопрос гарантий: старый код увидит в таком union обычную иерархию классов и не сможет обеспечить проверку всех вариантов. Так что решение крутое и рабочее, но всё же с оговорками.
Можно было еще про closed hierarchies proposal рассказать, кмк для примера с фигурами он был бы выразительнее
интересно как вы собираетесь работать с полученной структурой? Кажется в любом случае вам придется работать с конкретной структурой из union на каком бы языке вы это не писали:
public struct Shape
{
public ShapeType tag;
public double radius;
public (double Width, double Height) Rect;
}у вас в любом случае будет код извлечения (выбор ветки), и дальше две (N в общем случае) веток которые работают либо с radius
либо с Width,Height в этом случае. Это никогда не будет отличаться от того что придумали в Си. Работа через union это самый прямолинейный подход, который можно предложить.
Ну естественно ничего нового не будет и всё в итоге сводится к выбору нужной ветки. Другой вопрос в том, гарантирует ли кто-то, что все варианты обработаны.
Плюс я не понял к чему замечание на счёт того, что и так и сяк будет if else / switch, если в примерах демонстрировались разные способы расположения в памяти
в примерах демонстрировались разные способы расположения в памяти
по моему вариант один: сначала идет код типа структуры, а за ним сама структура.
Другой вопрос в том, гарантирует ли кто-то, что все варианты обработаны.
если мы пишем код который "знает" N типов структур и есть чужой код который генерирует эти структуры разного типа и нам присылает, у нас никогда не будет гарантии что этот чужой код не создаст новый N+1 тип структуры. Никто вам этого не гарантирует, на уровне языка программирования уж точно!
по моему вариант один: сначала идет код типа структуры, а за ним сама структура.
Вообще глобально 2 варианта: либо код типа и структура либо наследование классов.
у нас никогда не будет гарантии что этот чужой код не создаст новый N+1 тип структуры
Разве это проблема нас? если на входе невалидный объект, то всё что мы можем так это проигнорировать или бросить исключение. Вообще никогда нет гарантии, что на вход придут валидные данные, просто важно иметь механизм, который будет делать проверку того, что нам подкинули.
чужой код который генерирует эти структуры разного типа и нам присылает
Чужой код же по-хорошему тоже должен как-то валидировать то, что отправляет.
Чужой код же по-хорошему тоже должен как-то валидировать то, что отправляет.
вот именно что КТО-то должен, это организационный вопрос, административный, ... какой угодно, но не вопрос возможностей языка программирования! Вы можете даже не знать на каком языке написан чужой код, но ваш код не должен хотя бы крешится при поступлении неизвестного (со своей стороны) типа данных, это фундаментальное в своей примитивности требование, которое простейшим способом удовлетворяется, практически на любом языке.
Конечно, это само собой понятно, если, например, поступает объект с неверным тегом, то нужно иметь механизмы, которые защитят от неопределённого поведения. На самом деле единственный способ защититься это каждый раз делать проверку: при обращении к свойству условно под номером 1 нужно проверить, что тег равен 1, лишние проверки компилятор уберёт, а безопасность не будет нарушена. Таким образом даже если поступит объект с невалидным тегом, программа не упадёт
Он инструменты набирает популярность
Небольшая очепятка
Проблема ExplicitUnion в атомарности - вы должны и значение и тэг поменять атомарно, иначе это тайп-сефити баг потенциальный. А учитывая что больше чем 64 бита атомарно не поменять…
Думаю не надо объяснять что object и не-обджект в одном слоте вообще даже теоретически нельзя хранить в .NET
Поэтому структура должна быть readonly, этого хватило бы для решения всех гонок данных
Никаким образом это не решает проблему. Представьте что у вас поле класса - юнион Pet pet. вы меняете значение этого поля с Cat на Dog. в каком порядке вы должны менять тэг и shared слот чтобы читающий это поле другой поток всё это увидел без потенциального краша рантайма?
Если такая структура лежит как поле, то вы правы, но эта проблема касается вообще всех составных структур. И вроде все спокойно живут, нужно лишь использовать инструменты синхронизации, но сама структура этим не обязана заниматься
Нет. Присвоение структурного поля никогда не приведет к крашу рантайма, там не будет атомарности как это и обещает модель памяти, но никаких крашей рантайма не будет. А здесь без синхронизации может возникнуть ситуация когда вызывается инстанс метод (или например поле) у объекта, которые еще не успел переключится в новый объект (или наоборот - объект уже новый, а тэг старый).
Обычно структуры не берут на себя заботу о синхронизации. Тот же системный GUID в 16 байт нельзя атомарно записать, от этого он не становится плохим
Вы не внимательно прочитали мой комментарий. Если вы записываете гуид и читаете в потоках, то максимум что вам грозит - это прочтение гуида где только часть записалась, а остальное - нули/мусор. Возможно это приведет к ошибкам в логике программы - но это уже на совести программиста. Несинхранизированная запись юниона, в котором вы делаете Unsafe.As над shared слотом в зависимости от тэга может привести к крашу рантайма. Фактически, это UB для джита/рантайма.
На самом деле вряд ли, ведь там нет ссылок на объекты внутри, а значит всё также будут просто криво прочитанные данные
из примера выше:
public record class Cat(string Name);
public record class Dog(string Name);
public union Pet(Cat, Dog);
где тут нет объектов?
Так а почему не сделать просто интерфейс и его реализации?
Интересно узнать, для решения каких предметных задач строится такой сложный "огород" ?
Да в целом много где, напишу, что вспомнил:
Функция, которая вместо исключения выдаёт либо результат либо подробность ошибки (не нужно аллоцировать память на исключение и разворачивать стек)
Веб, где можно к примеру было бы вернуть 200 и страницу либо условно 404 (не найдено)
Всякие компиляторы и парсеры, например, для литералов, который может быть как числом, так и строкой
Состояние игрока в игре: например Moving (vector3), attacking (enemyId) или idle
И лично я в своей игре это использовал для дипломатии, где получателем может быть либо страна, либо альянс, а так же для дивизий, который либо в регионе на суше, либо в морском
В общем применений у DU много, просто не всем приходится с этим сталкиваться
Для 2, 4 случаев и ниже для игр ничего сказать не могу, тк этим не занимался. Для 1-ого случая при сложном алгоритме для надежности кода программной единицы (функции, модуля) желательно сразу на входе делать проверку корректности значений входных параметров для которых будет работать этот алгоритм и выдавать сообщение вызывающей единице с записью в системный errors.log. То есть "на берегу" исключать появление run-time error. Дальше все зависит от самого алгоритма и правильности его представления на ЯП. Из классики надёжного программирования рекомендуется размер процедур и функций ограничивать не более 50-100 строками ЯП. Для 3-его применения в системном программировании (при разработке компиляторов, интерпретаторов, СУБД) описанные Вами приёмы оправданы, самому приходилось складывать в байтовую память а затем извлекать из неё наборы разнотипных данных. Но, согласитесь, такие потребности возникают "не каждый день". Все задачи прикладного программирования, для обеспечения надёжности и понятности программного кода, решаются стандартными возможностями ЯП без всяких ухищрений.
Попытка добавить то, что из коробки отсутствует в языке. Это как в чисто объекто-ориентированный язык пытаться добавить возможности функционального языка. Вот и выходит кривое-косое.
Интересный вариант.
Discriminated Unions: Что не так с реализациями объединений в C#?