Дискриминационный союз в C#
[Примечание: у этого вопроса было первоначальное название "объединение стилей C (ish) в C#", но, как сообщил мне комментарий Джеффа, очевидно, что эта структура называется "дискриминационным объединением"]
Извините за многословность этого вопроса.
В SO уже есть несколько похожих вопросов, но они, похоже, концентрируются на преимуществах объединения памяти или использования его для взаимодействия. Вот пример такого вопроса.
Мое желание иметь профсоюзную вещь несколько отличается.
Я сейчас пишу некоторый код, который генерирует объекты, которые немного похожи на это
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
Довольно сложные вещи, я думаю, вы согласитесь. Дело в том, что ValueA
может быть только нескольких определенных типов (скажем, string
, int
а также Foo
(который является классом) и ValueB
может быть другой небольшой набор типов. Мне не нравится рассматривать эти значения как объекты (я хочу теплое уютное чувство кодирования с некоторой безопасностью типов).
Так что я подумал о написании тривиального небольшого класса-обертки, чтобы выразить тот факт, что ValueA логически является ссылкой на определенный тип. Я позвонил в класс Union
потому что то, что я пытаюсь достичь, напомнило мне о концепции объединения в C.
public class Union<A, B, C>
{
private readonly Type type;
public readonly A a;
public readonly B b;
public readonly C c;
public A A{get {return a;}}
public B B{get {return b;}}
public C C{get {return c;}}
public Union(A a)
{
type = typeof(A);
this.a = a;
}
public Union(B b)
{
type = typeof(B);
this.b = b;
}
public Union(C c)
{
type = typeof(C);
this.c = c;
}
/// <summary>
/// Returns true if the union contains a value of type T
/// </summary>
/// <remarks>The type of T must exactly match the type</remarks>
public bool Is<T>()
{
return typeof(T) == type;
}
/// <summary>
/// Returns the union value cast to the given type.
/// </summary>
/// <remarks>If the type of T does not exactly match either X or Y, then the value <c>default(T)</c> is returned.</remarks>
public T As<T>()
{
if(Is<A>())
{
return (T)(object)a; // Is this boxing and unboxing unavoidable if I want the union to hold value types and reference types?
//return (T)x; // This will not compile: Error = "Cannot cast expression of type 'X' to 'T'."
}
if(Is<B>())
{
return (T)(object)b;
}
if(Is<C>())
{
return (T)(object)c;
}
return default(T);
}
}
Использование этого класса ValueWrapper теперь выглядит так
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
что-то вроде того, чего я хотел достичь, но я упускаю один довольно важный элемент - это принудительная проверка типов при вызове функций Is и As, как показано в следующем коде
public void DoSomething()
{
if(ValueA.Is<string>())
{
var s = ValueA.As<string>();
// .... do somethng
}
if(ValueA.Is<char>()) // I would really like this to be a compile error
{
char c = ValueA.As<char>();
}
}
IMO Неверно спрашивать ValueA, если это char
поскольку в его определении четко сказано, что это не так - это ошибка программирования, и я хотел бы, чтобы компилятор воспользовался этим. [Также, если бы я мог сделать это правильно, то (надеюсь), я бы также получил intellisense - что было бы благом.]
Чтобы добиться этого, я бы хотел сказать компилятору, что тип T
может быть одним из A, B или C
public bool Is<T>() where T : A
or T : B // Yes I know this is not legal!
or T : C
{
return typeof(T) == type;
}
У кого-нибудь есть идеи, возможно ли то, чего я хочу достичь? Или я просто тупой, чтобы писать этот класс?
Заранее спасибо.
17 ответов
Мне не очень нравятся решения для проверки типов и приведения типов, представленные выше, поэтому вот 100% -ое безопасное для типов объединение, которое будет выдавать ошибки компиляции, если вы попытаетесь использовать неправильный тип данных:
using System;
namespace Juliet
{
class Program
{
static void Main(string[] args)
{
Union3<int, char, string>[] unions = new Union3<int,char,string>[]
{
new Union3<int, char, string>.Case1(5),
new Union3<int, char, string>.Case2('x'),
new Union3<int, char, string>.Case3("Juliet")
};
foreach (Union3<int, char, string> union in unions)
{
string value = union.Match(
num => num.ToString(),
character => new string(new char[] { character }),
word => word);
Console.WriteLine("Matched union with value '{0}'", value);
}
Console.ReadLine();
}
}
public abstract class Union3<A, B, C>
{
public abstract T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h);
// private ctor ensures no external classes can inherit
private Union3() { }
public sealed class Case1 : Union3<A, B, C>
{
public readonly A Item;
public Case1(A item) : base() { this.Item = item; }
public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
{
return f(Item);
}
}
public sealed class Case2 : Union3<A, B, C>
{
public readonly B Item;
public Case2(B item) { this.Item = item; }
public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
{
return g(Item);
}
}
public sealed class Case3 : Union3<A, B, C>
{
public readonly C Item;
public Case3(C item) { this.Item = item; }
public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
{
return h(Item);
}
}
}
}
Мне нравится направление принятого решения, но оно плохо масштабируется для объединений из более чем трех элементов (например, объединение из 9 элементов потребовало бы 9 определений классов).
Вот еще один подход, который также на 100% безопасен для типов во время компиляции, но его легко превратить в большие объединения.
public class UnionBase<A>
{
dynamic value;
public UnionBase(A a) { value = a; }
protected UnionBase(object x) { value = x; }
protected T InternalMatch<T>(params Delegate[] ds)
{
var vt = value.GetType();
foreach (var d in ds)
{
var mi = d.Method;
// These are always true if InternalMatch is used correctly.
Debug.Assert(mi.GetParameters().Length == 1);
Debug.Assert(typeof(T).IsAssignableFrom(mi.ReturnType));
var pt = mi.GetParameters()[0].ParameterType;
if (pt.IsAssignableFrom(vt))
return (T)mi.Invoke(null, new object[] { value });
}
throw new Exception("No appropriate matching function was provided");
}
public T Match<T>(Func<A, T> fa) { return InternalMatch<T>(fa); }
}
public class Union<A, B> : UnionBase<A>
{
public Union(A a) : base(a) { }
public Union(B b) : base(b) { }
protected Union(object x) : base(x) { }
public T Match<T>(Func<A, T> fa, Func<B, T> fb) { return InternalMatch<T>(fa, fb); }
}
public class Union<A, B, C> : Union<A, B>
{
public Union(A a) : base(a) { }
public Union(B b) : base(b) { }
public Union(C c) : base(c) { }
protected Union(object x) : base(x) { }
public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc) { return InternalMatch<T>(fa, fb, fc); }
}
public class Union<A, B, C, D> : Union<A, B, C>
{
public Union(A a) : base(a) { }
public Union(B b) : base(b) { }
public Union(C c) : base(c) { }
public Union(D d) : base(d) { }
protected Union(object x) : base(x) { }
public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd) { return InternalMatch<T>(fa, fb, fc, fd); }
}
public class Union<A, B, C, D, E> : Union<A, B, C, D>
{
public Union(A a) : base(a) { }
public Union(B b) : base(b) { }
public Union(C c) : base(c) { }
public Union(D d) : base(d) { }
public Union(E e) : base(e) { }
protected Union(object x) : base(x) { }
public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd, Func<E, T> fe) { return InternalMatch<T>(fa, fb, fc, fd, fe); }
}
public class DiscriminatedUnionTest : IExample
{
public Union<int, bool, string, int[]> MakeUnion(int n)
{
return new Union<int, bool, string, int[]>(n);
}
public Union<int, bool, string, int[]> MakeUnion(bool b)
{
return new Union<int, bool, string, int[]>(b);
}
public Union<int, bool, string, int[]> MakeUnion(string s)
{
return new Union<int, bool, string, int[]>(s);
}
public Union<int, bool, string, int[]> MakeUnion(params int[] xs)
{
return new Union<int, bool, string, int[]>(xs);
}
public void Print(Union<int, bool, string, int[]> union)
{
var text = union.Match(
n => "This is an int " + n.ToString(),
b => "This is a boolean " + b.ToString(),
s => "This is a string" + s,
xs => "This is an array of ints " + String.Join(", ", xs));
Console.WriteLine(text);
}
public void Run()
{
Print(MakeUnion(1));
Print(MakeUnion(true));
Print(MakeUnion("forty-two"));
Print(MakeUnion(0, 1, 1, 2, 3, 5, 8));
}
}
Я написал библиотеку для этого на https://github.com/mcintyre321/OneOf
Установить-пакет OneOf
В нем есть общие типы для выполнения DU, например OneOf<T0, T1>
вплоть до OneOf<T0, ..., T9>
, Каждый из них имеет .Match
и .Switch
оператор, который вы можете использовать для безопасного типизированного поведения компилятора, например:
`` `
OneOf<string, ColorName, Color> backgroundColor = getBackground();
Color c = backgroundColor.Match(
str => CssHelper.GetColorFromString(str),
name => new Color(name),
col => col
);
`` `
Хотя это старый вопрос, я недавно написал несколько постов в блоге на эту тему, которые могут быть полезны.
Допустим, у вас есть сценарий корзины покупок с тремя состояниями: "Пусто", "Активно" и "Оплачено", каждое с разным поведением.
- Вы создаете
ICartState
интерфейс, который объединяет все состояния (и это может быть просто пустой интерфейс маркера) - Вы создаете три класса, которые реализуют этот интерфейс. (Классы не должны быть в отношениях наследования)
- Интерфейс содержит метод "fold", посредством которого вы передаете лямбду для каждого состояния или случая, которые вам нужно обработать.
Вы можете использовать среду выполнения F# из C#, но в качестве более легкой альтернативы я написал небольшой шаблон T4 для генерации кода, подобного этому.
Вот интерфейс:
partial interface ICartState
{
ICartState Transition(
Func<CartStateEmpty, ICartState> cartStateEmpty,
Func<CartStateActive, ICartState> cartStateActive,
Func<CartStatePaid, ICartState> cartStatePaid
);
}
И вот реализация:
class CartStateEmpty : ICartState
{
ICartState ICartState.Transition(
Func<CartStateEmpty, ICartState> cartStateEmpty,
Func<CartStateActive, ICartState> cartStateActive,
Func<CartStatePaid, ICartState> cartStatePaid
)
{
// I'm the empty state, so invoke cartStateEmpty
return cartStateEmpty(this);
}
}
class CartStateActive : ICartState
{
ICartState ICartState.Transition(
Func<CartStateEmpty, ICartState> cartStateEmpty,
Func<CartStateActive, ICartState> cartStateActive,
Func<CartStatePaid, ICartState> cartStatePaid
)
{
// I'm the active state, so invoke cartStateActive
return cartStateActive(this);
}
}
class CartStatePaid : ICartState
{
ICartState ICartState.Transition(
Func<CartStateEmpty, ICartState> cartStateEmpty,
Func<CartStateActive, ICartState> cartStateActive,
Func<CartStatePaid, ICartState> cartStatePaid
)
{
// I'm the paid state, so invoke cartStatePaid
return cartStatePaid(this);
}
}
Теперь скажем, вы продлите CartStateEmpty
а также CartStateActive
с AddItem
метод, который не реализован CartStatePaid
,
А также скажем, что CartStateActive
имеет Pay
метод, которого нет в других штатах.
Тогда вот некоторый код, который показывает, что он используется - добавление двух товаров и затем оплата за корзину:
public ICartState AddProduct(ICartState currentState, Product product)
{
return currentState.Transition(
cartStateEmpty => cartStateEmpty.AddItem(product),
cartStateActive => cartStateActive.AddItem(product),
cartStatePaid => cartStatePaid // not allowed in this case
);
}
public void Example()
{
var currentState = new CartStateEmpty() as ICartState;
//add some products
currentState = AddProduct(currentState, Product.ProductX);
currentState = AddProduct(currentState, Product.ProductY);
//pay
const decimal paidAmount = 12.34m;
currentState = currentState.Transition(
cartStateEmpty => cartStateEmpty, // not allowed in this case
cartStateActive => cartStateActive.Pay(paidAmount),
cartStatePaid => cartStatePaid // not allowed in this case
);
}
Обратите внимание, что этот код полностью безопасен для типов - нигде нет приведения или условных выражений, и, скажем, ошибки компилятора, если вы пытаетесь заплатить за пустую корзину.
Я не уверен, что полностью понимаю вашу цель. В C объединение - это структура, которая использует одни и те же ячейки памяти для нескольких полей. Например:
typedef union
{
float real;
int scalar;
} floatOrScalar;
floatOrScalar
union может использоваться как float или int, но они оба занимают одно и то же пространство памяти. Изменение одного меняет другое. Вы можете добиться того же с помощью структуры в C#:
[StructLayout(LayoutKind.Explicit)]
struct FloatOrScalar
{
[FieldOffset(0)]
public float Real;
[FieldOffset(0)]
public int Scalar;
}
Вышеупомянутая структура использует всего 32 бита, а не 64 бита. Это возможно только с помощью структуры. Ваш приведенный выше пример является классом, и, учитывая характер CLR, не дает никаких гарантий эффективности памяти. Если вы измените Union<A, B, C>
из одного типа в другой, вы не обязательно повторно используете память... скорее всего, вы выделяете новый тип в куче и отбрасываете другой указатель в основу object
поле. Вопреки настоящему союзу, ваш подход может привести к большому количеству кучи, чем вы могли бы получить, если бы не использовали свой тип объединения.
И моя попытка минимального, но расширяемого решения с использованием вложенности типа Union/Either. Также использование параметров по умолчанию в методе Match, естественно, включает сценарий "Или X или по умолчанию".
using System;
using System.Reflection;
using NUnit.Framework;
namespace Playground
{
[TestFixture]
public class EitherTests
{
[Test]
public void Test_Either_of_Property_or_FieldInfo()
{
var some = new Some(false);
var field = some.GetType().GetField("X");
var property = some.GetType().GetProperty("Y");
Assert.NotNull(field);
Assert.NotNull(property);
var info = Either<PropertyInfo, FieldInfo>.Of(field);
var infoType = info.Match(p => p.PropertyType, f => f.FieldType);
Assert.That(infoType, Is.EqualTo(typeof(bool)));
}
[Test]
public void Either_of_three_cases_using_nesting()
{
var some = new Some(false);
var field = some.GetType().GetField("X");
var parameter = some.GetType().GetConstructors()[0].GetParameters()[0];
Assert.NotNull(field);
Assert.NotNull(parameter);
var info = Either<ParameterInfo, Either<PropertyInfo, FieldInfo>>.Of(parameter);
var name = info.Match(_ => _.Name, _ => _.Name, _ => _.Name);
Assert.That(name, Is.EqualTo("a"));
}
public class Some
{
public bool X;
public string Y { get; set; }
public Some(bool a)
{
X = a;
}
}
}
public static class Either
{
public static T Match<A, B, C, T>(
this Either<A, Either<B, C>> source,
Func<A, T> a = null, Func<B, T> b = null, Func<C, T> c = null)
{
return source.Match(a, bc => bc.Match(b, c));
}
}
public abstract class Either<A, B>
{
public static Either<A, B> Of(A a)
{
return new CaseA(a);
}
public static Either<A, B> Of(B b)
{
return new CaseB(b);
}
public abstract T Match<T>(Func<A, T> a = null, Func<B, T> b = null);
private sealed class CaseA : Either<A, B>
{
private readonly A _item;
public CaseA(A item) { _item = item; }
public override T Match<T>(Func<A, T> a = null, Func<B, T> b = null)
{
return a == null ? default(T) : a(_item);
}
}
private sealed class CaseB : Either<A, B>
{
private readonly B _item;
public CaseB(B item) { _item = item; }
public override T Match<T>(Func<A, T> a = null, Func<B, T> b = null)
{
return b == null ? default(T) : b(_item);
}
}
}
}
Если вы разрешаете несколько типов, вы не можете достичь безопасности типов (если типы не связаны).
Вы не можете и не достигнете никакой безопасности типов, вы можете достичь безопасности байтовых значений только с помощью FieldOffset.
Было бы гораздо больше смысла иметь общий ValueWrapper<T1, T2>
с T1 ValueA
а также T2 ValueB
...
PS: когда речь идет о безопасности типов, я имею в виду безопасность типов во время компиляции.
Если вам нужна обертка кода (выполняя бизнес-логику на модификациях, вы можете использовать что-то вроде:
public class Wrapper
{
public ValueHolder<int> v1 = 5;
public ValueHolder<byte> v2 = 8;
}
public struct ValueHolder<T>
where T : struct
{
private T value;
public ValueHolder(T value) { this.value = value; }
public static implicit operator T(ValueHolder<T> valueHolder) { return valueHolder.value; }
public static implicit operator ValueHolder<T>(T value) { return new ValueHolder<T>(value); }
}
Для простого выхода вы можете использовать (у него проблемы с производительностью, но он очень прост):
public class Wrapper
{
private object v1;
private object v2;
public T GetValue1<T>() { if (v1.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v1; }
public void SetValue1<T>(T value) { v1 = value; }
public T GetValue2<T>() { if (v2.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v2; }
public void SetValue2<T>(T value) { v2 = value; }
}
//usage:
Wrapper wrapper = new Wrapper();
wrapper.SetValue1("aaaa");
wrapper.SetValue2(456);
string s = wrapper.GetValue1<string>();
DateTime dt = wrapper.GetValue1<DateTime>();//InvalidCastException
char foo = 'B';
bool bar = foo is int;
Это приводит к предупреждению, а не к ошибке. Если вы ищете свой Is
а также As
функции должны быть аналогами для операторов C#, тогда вы не должны так ограничивать их.
Вот моя попытка. Он компилирует проверку времени типов, используя общие ограничения типов.
class Union {
public interface AllowedType<T> { };
internal object val;
internal System.Type type;
}
static class UnionEx {
public static T As<U,T>(this U x) where U : Union, Union.AllowedType<T> {
return x.type == typeof(T) ?(T)x.val : default(T);
}
public static void Set<U,T>(this U x, T newval) where U : Union, Union.AllowedType<T> {
x.val = newval;
x.type = typeof(T);
}
public static bool Is<U,T>(this U x) where U : Union, Union.AllowedType<T> {
return x.type == typeof(T);
}
}
class MyType : Union, Union.AllowedType<int>, Union.AllowedType<string> {}
class TestIt
{
static void Main()
{
MyType bla = new MyType();
bla.Set(234);
System.Console.WriteLine(bla.As<MyType,int>());
System.Console.WriteLine(bla.Is<MyType,string>());
System.Console.WriteLine(bla.Is<MyType,int>());
bla.Set("test");
System.Console.WriteLine(bla.As<MyType,string>());
System.Console.WriteLine(bla.Is<MyType,string>());
System.Console.WriteLine(bla.Is<MyType,int>());
// compile time errors!
// bla.Set('a');
// bla.Is<MyType,char>()
}
}
Можно было бы немного покрасоваться. В частности, я не мог понять, как избавиться от параметров типа в As/Is/Set (разве нет способа указать один параметр типа и позволить C# вычислять другой?)
Поэтому я неоднократно сталкивался с одной и той же проблемой, и я просто нашел решение, которое получает нужный мне синтаксис (за счет некоторого уродства при реализации типа Union).
Напомним: мы хотим такого рода использование на сайте вызова.
Union<int, string> u;
u = 1492;
int yearColumbusDiscoveredAmerica = u;
u = "hello world";
string traditionalGreeting = u;
var answers = new SortedList<string, Union<int, string, DateTime>>();
answers["life, the universe, and everything"] = 42;
answers["D-Day"] = new DateTime(1944, 6, 6);
answers["C#"] = "is awesome";
Однако мы хотим, чтобы следующие примеры не компилировались, так что мы получим хоть немного безопасности типов.
DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;
Для дополнительного кредита, давайте также не будем занимать больше места, чем абсолютно необходимо.
С учетом всего сказанного, вот моя реализация для двух параметров общего типа. Реализация трех, четырех и т. Д. Параметров типа проста.
public abstract class Union<T1, T2>
{
public abstract int TypeSlot
{
get;
}
public virtual T1 AsT1()
{
throw new TypeAccessException(string.Format(
"Cannot treat this instance as a {0} instance.", typeof(T1).Name));
}
public virtual T2 AsT2()
{
throw new TypeAccessException(string.Format(
"Cannot treat this instance as a {0} instance.", typeof(T2).Name));
}
public static implicit operator Union<T1, T2>(T1 data)
{
return new FromT1(data);
}
public static implicit operator Union<T1, T2>(T2 data)
{
return new FromT2(data);
}
public static implicit operator Union<T1, T2>(Tuple<T1, T2> data)
{
return new FromTuple(data);
}
public static implicit operator T1(Union<T1, T2> source)
{
return source.AsT1();
}
public static implicit operator T2(Union<T1, T2> source)
{
return source.AsT2();
}
private class FromT1 : Union<T1, T2>
{
private readonly T1 data;
public FromT1(T1 data)
{
this.data = data;
}
public override int TypeSlot
{
get { return 1; }
}
public override T1 AsT1()
{
return this.data;
}
public override string ToString()
{
return this.data.ToString();
}
public override int GetHashCode()
{
return this.data.GetHashCode();
}
}
private class FromT2 : Union<T1, T2>
{
private readonly T2 data;
public FromT2(T2 data)
{
this.data = data;
}
public override int TypeSlot
{
get { return 2; }
}
public override T2 AsT2()
{
return this.data;
}
public override string ToString()
{
return this.data.ToString();
}
public override int GetHashCode()
{
return this.data.GetHashCode();
}
}
private class FromTuple : Union<T1, T2>
{
private readonly Tuple<T1, T2> data;
public FromTuple(Tuple<T1, T2> data)
{
this.data = data;
}
public override int TypeSlot
{
get { return 0; }
}
public override T1 AsT1()
{
return this.data.Item1;
}
public override T2 AsT2()
{
return this.data.Item2;
}
public override string ToString()
{
return this.data.ToString();
}
public override int GetHashCode()
{
return this.data.GetHashCode();
}
}
}
Вы можете генерировать исключения, если есть попытка получить доступ к переменным, которые не были инициализированы, то есть, если они созданы с параметром A, а затем есть попытка получить доступ к B или C, это может вызвать, скажем, UnsupportedOperationException. Вам нужен геттер, чтобы заставить его работать, хотя.
Вот мое решение, которое, как мне кажется, охватывает все основные случаи использования. Он полностью проверен на типизацию и допускает функции is, as и switch. Добавление объединений более чем 4 возможных типов потребует просто добавления еще одного подкласса.
public interface IUnionOf<out T>
{
bool Is();
T? As();
}
public abstract class Union<T1> : IUnionOf<T1>
{
protected readonly object? value;
protected Union(object? value)
{
this.value = value;
}
bool IUnionOf<T1>.Is() => value is T1;
T1? IUnionOf<T1>.As() => value is T1 v2 ? v2 : default;
}
public class Union<T1, T2> : Union<T1>, IUnionOf<T2>
{
protected Union(object? value) : base(value)
{
}
bool IUnionOf<T2>.Is() => value is T2;
T2? IUnionOf<T2>.As() => value is T2 v2 ? v2 : default;
public void Switch(Action<T1> t1, Action<T2> t2)
{
switch (value)
{
case T1 v1 : t1(v1); break;
case T2 v2 : t2(v2); break;
}
}
public static implicit operator Union<T1, T2>(T1 value) => new(value);
public static implicit operator Union<T1, T2>(T2 value) => new(value);
}
public class Union<T1, T2, T3> : Union<T1, T2>, IUnionOf<T3>
{
protected Union(object? value) : base(value)
{
}
bool IUnionOf<T3>.Is() => value is T3;
T3? IUnionOf<T3>.As() => value is T3 v3 ? v3 : default;
public void Switch(Action<T1> t1, Action<T2> t2, Action<T3> t3)
{
switch (value)
{
case T1 v1: t1(v1); break;
case T2 v2: t2(v2); break;
case T3 v3: t3(v3); break;
}
}
public static implicit operator Union<T1, T2, T3>(T1 value) => new(value);
public static implicit operator Union<T1, T2, T3>(T2 value) => new(value);
public static implicit operator Union<T1, T2, T3>(T3 value) => new(value);
}
public static class UnionExtensions
{
public static bool Is<T>(this IUnionOf<T> union) => union.Is();
public static T? As<T>(this IUnionOf<T> union) => union.As();
}
Неявные преобразования позволяют производителям использовать их очень естественно:
public Union<string, int> GetStringOrInt(bool getString)
{
if (getString)
{
return "Hello";
}
else
{
return 39;
}
}
А методы расширения допускают естественное использование потребителями:
var result = GetStringOrInt(true);
if (result.Is<string>())
{
// It's a string!
}
var stringValue = result.As<string>();
// In this case, intValue is 0 because As() returns default if the type doesn't match
var intValue = result.As<intValue>();
result.Switch(
stringValue =>
{
// Do something with stringValue
},
intValue =>
{
// Do something with intValue
}
);
// This does not compile
if (result.Is<double>())
{
}
Группа разработчиков языка C# обсудила дискриминационные союзы в январе 2017 года https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Вы можете проголосовать за запрос функции на https://github.com/dotnet/csharplang/issues/113
В настоящее время я пытаюсь создать среду выполнения Julia в .NET. У Джулии есть такие типы, как Union{Int, String}... и т. д. В настоящее время я пытаюсь смоделировать этот .NET (без странного IL, который нельзя было бы вызвать из С#).
Вот реализация объединения структур во время компиляции. Я буду создавать больше объединений для объединений объектов, а также перекрестных объединений объектов и структур (это будет самый сложный случай).
public struct Union<T1,T2> where T1 : struct where T2 : struct{
private byte type;
[FieldOffset(1)] private T1 a1;
[FieldOffset(1)] private T2 a2;
public T1 A1 {
get => a1;
set {
a1 = value;
type = 1;
}
}
public T2 A2 {
get => a2;
set {
a2 = value;
type = 2;
}
}
public Union(int _ = 0) {
type = 0;
a1 = default;
a2 = default;
}
public Union(T1 a) : this() => A1 = a;
public Union(T2 a) : this() => A2 = a;
public bool HasValue => type < 1 || type > 2;
public bool IsNull => !HasValue;
public bool IsT1 => type == 1;
public bool IsT2 => type == 2;
public Type GetType() {
switch (type) {
case 1: return typeof(T1);
case 2: return typeof(T2);
default: return null;
}
}
}
Вы можете использовать вышеизложенное следующим образом:
Union<int, long> myUnion(5); \\Set int inside
myUnion.a2 = 5;
Type theTypeInside = myUnion.GetType(); //long
myUnion.a1 = 5;
theTypeInside = myUnion.GetType(); //int
Я также буду создавать генераторы динамических объединений или выровненные объединения для перекрестного объекта и объединения структур.
Взгляните на:Generated Struct Union Output , чтобы увидеть текущие объединения времени компиляции, которые я использую.
Если вы хотите создать союз любого размера, взгляните на Генератор структурных союзов.
Если у кого-то есть какие-либо улучшения для вышеперечисленного, дайте мне знать! Внедрение julia в .NET — чрезвычайно трудная задача!
Невозможно сделать именно тот синтаксис, который вы использовали, но с немного большей детализацией и копированием / вставкой легко сделать так, чтобы разрешение перегрузки сделало вашу работу:
// this code is ok
var u = new Union("");
if (u.Value(Is.OfType()))
{
u.Value(Get.ForType());
}
// and this one will not compile
if (u.Value(Is.OfType()))
{
u.Value(Get.ForType());
}
К настоящему времени должно быть довольно очевидно, как это реализовать:
public class Union
{
private readonly Type type;
public readonly A a;
public readonly B b;
public readonly C c;
public Union(A a)
{
type = typeof(A);
this.a = a;
}
public Union(B b)
{
type = typeof(B);
this.b = b;
}
public Union(C c)
{
type = typeof(C);
this.c = c;
}
public bool Value(TypeTestSelector _)
{
return typeof(A) == type;
}
public bool Value(TypeTestSelector _)
{
return typeof(B) == type;
}
public bool Value(TypeTestSelector _)
{
return typeof(C) == type;
}
public A Value(GetValueTypeSelector _)
{
return a;
}
public B Value(GetValueTypeSelector _)
{
return b;
}
public C Value(GetValueTypeSelector _)
{
return c;
}
}
public static class Is
{
public static TypeTestSelector OfType()
{
return null;
}
}
public class TypeTestSelector
{
}
public static class Get
{
public static GetValueTypeSelector ForType()
{
return null;
}
}
public class GetValueTypeSelector
{
}
Нет никаких проверок для извлечения значения неправильного типа, например:
var u = Union(10);
string s = u.Value(Get.ForType());
Таким образом, вы можете рассмотреть возможность добавления необходимых проверок и исключения в таких случаях.
Вы можете экспортировать функцию сопоставления псевдо-паттернов, как я использую для типа Either в моей библиотеке Sasa. В настоящее время существуют накладные расходы времени выполнения, но я в конечном итоге планирую добавить анализ CIL, чтобы встроить всех делегатов в истинный оператор case.
Я использую собственный Union Type.
Рассмотрим пример, чтобы прояснить ситуацию.
Представьте, что у нас есть контактный класс:
public class Contact
{
public string Name { get; set; }
public string EmailAddress { get; set; }
public string PostalAdrress { get; set; }
}
Все они определены как простые строки, но на самом деле это просто строки? Конечно, нет. Имя может состоять из имени и фамилии. Или электронная почта - это просто набор символов? Я знаю, что по крайней мере он должен содержать @, и это обязательно.
Давайте улучшим нам модель домена
public class PersonalName
{
public PersonalName(string firstName, string lastName) { ... }
public string Name() { return _fistName + " " _lastName; }
}
public class EmailAddress
{
public EmailAddress(string email) { ... }
}
public class PostalAdrress
{
public PostalAdrress(string address, string city, int zip) { ... }
}
В этом классе будут проверки во время создания, и мы в конечном итоге будем иметь действительные модели. Consturctor в классе PersonaName требует имен FirstName и LastName одновременно. Это означает, что после создания он не может иметь недопустимое состояние.
И класс контакта соответственно
public class Contact
{
public PersonalName Name { get; set; }
public EmailAdress EmailAddress { get; set; }
public PostalAddress PostalAddress { get; set; }
}
В этом случае у нас та же проблема, объект класса Contact может быть в недопустимом состоянии. Я имею в виду, что он может иметь адрес электронной почты, но не имеет имени
var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };
Давайте исправим это и создадим класс Contact с конструктором, который требует PersonalName, EmailAddress и PostalAddress:
public class Contact
{
public Contact(
PersonalName personalName,
EmailAddress emailAddress,
PostalAddress postalAddress
)
{
...
}
}
Но здесь у нас есть другая проблема. Что делать, если у Person есть только EmailAdress и нет PostalAddress?
Если мы подумаем об этом, мы поймем, что существует три возможности правильного состояния объекта класса Contact:
- Контакт имеет только адрес электронной почты
- Контакт имеет только почтовый адрес
- Контакт имеет адрес электронной почты и почтовый адрес
Давайте выпишем доменные модели. Для начала создадим класс Contact Info, состояние которого будет соответствовать вышеуказанным случаям.
public class ContactInfo
{
public ContactInfo(EmailAddress emailAddress) { ... }
public ContactInfo(PostalAddress postalAddress) { ... }
public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... }
}
И контактный класс:
public class Contact
{
public Contact(
PersonalName personalName,
ContactInfo contactInfo
)
{
...
}
}
Давайте попробуем использовать это:
var contact = new Contact(
new PersonalName("James", "Bond"),
new ContactInfo(
new EmailAddress("agent@007.com")
)
);
Console.WriteLine(contact.PersonalName()); // James Bond
Console.WriteLine(contact.ContactInfo().???) // here we have problem, because ContactInfo have three possible state and if we want print it we would write `if` cases
Давайте добавим метод Match в класс ContactInfo
public class ContactInfo
{
// constructor
public TResult Match<TResult>(
Func<EmailAddress,TResult> f1,
Func<PostalAddress,TResult> f2,
Func<Tuple<EmailAddress,PostalAddress>> f3
)
{
if (_emailAddress != null)
{
return f1(_emailAddress);
}
else if(_postalAddress != null)
{
...
}
...
}
}
В методе match мы можем написать этот код, потому что состояние класса контакта контролируется конструкторами и может иметь только одно из возможных состояний.
Давайте создадим вспомогательный класс, чтобы каждый раз не писать столько кода.
public abstract class Union<T1,T2,T3>
where T1 : class
where T2 : class
where T3 : class
{
private readonly T1 _t1;
private readonly T2 _t2;
private readonly T3 _t3;
public Union(T1 t1) { _t1 = t1; }
public Union(T2 t2) { _t2 = t2; }
public Union(T3 t3) { _t3 = t3; }
public TResult Match<TResult>(
Func<T1, TResult> f1,
Func<T2, TResult> f2,
Func<T3, TResult> f3
)
{
if (_t1 != null)
{
return f1(_t1);
}
else if (_t2 != null)
{
return f2(_t2);
}
else if (_t3 != null)
{
return f3(_t3);
}
throw new Exception("can't match");
}
}
Мы можем заранее иметь такой класс для нескольких типов, как это делается с делегатами Func, Action. 4-6 параметров универсального типа будут полностью для класса Union.
Давайте перепишем ContactInfo
учебный класс:
public sealed class ContactInfo : Union<
EmailAddress,
PostalAddress,
Tuple<EmaiAddress,PostalAddress>
>
{
public Contact(EmailAddress emailAddress) : base(emailAddress) { }
public Contact(PostalAddress postalAddress) : base(postalAddress) { }
public Contact(Tuple<EmaiAddress, PostalAddress> emailAndPostalAddress) : base(emailAndPostalAddress) { }
}
Здесь компилятор попросит переопределить хотя бы один конструктор. Если мы забудем переопределить остальные конструкторы, мы не сможем создать объект класса ContactInfo с другим состоянием. Это защитит нас от исключений во время выполнения.
var contact = new Contact(
new PersonalName("James", "Bond"),
new ContactInfo(
new EmailAddress("agent@007.com")
)
);
Console.WriteLine(contact.PersonalName()); // James Bond
Console
.WriteLine(
contact
.ContactInfo()
.Match(
(emailAddress) => emailAddress.Address,
(postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(),
(emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString()
)
);
Это все. Надеюсь, тебе понравилось.
Пример взят с сайта F# для прикола и прибыли