União discriminada em C #


92

[Observação: esta questão tinha o título original " União de estilo C (ish) em C # ", mas, como o comentário de Jeff me informou, aparentemente essa estrutura é chamada de 'união discriminada']

Desculpe a verbosidade desta pergunta.

Já existem algumas perguntas semelhantes às minhas no SO, mas elas parecem se concentrar nos benefícios de economia de memória do sindicato ou em usá-lo para interoperabilidade. Aqui está um exemplo de tal pergunta .

Meu desejo de ter um tipo de sindicato é um pouco diferente.

Estou escrevendo um código no momento que gera objetos que se parecem um pouco com este

public class ValueWrapper
{
    public DateTime ValueCreationDate;
    // ... other meta data about the value

    public object ValueA;
    public object ValueB;
}

Coisas muito complicadas, acho que você vai concordar. O fato é que ValueAsó podem ser de alguns tipos (digamos string, inte Foo(que é uma classe) e ValueBpodem ser outro pequeno conjunto de tipos. Não gosto de tratar esses valores como objetos (quero a sensação aconchegante de codificação com um pouco de segurança de tipo).

Portanto, pensei em escrever uma pequena classe de wrapper trivial para expressar o fato de que ValueA logicamente é uma referência a um tipo específico. Liguei para a classe Unionporque o que estou tentando alcançar me lembrou do conceito de união em 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);
    }
}

Usando esta classe ValueWrapper agora se parece com isto

public class ValueWrapper2
{
    public DateTime ValueCreationDate;
    public  Union<int, string, Foo> ValueA;
    public  Union<double, Bar, Foo> ValueB;
}

que é algo parecido com o que eu queria alcançar, mas estou faltando um elemento bastante crucial - que é a verificação de tipo forçada do compilador ao chamar as funções Is e As, como o código a seguir demonstra

    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 Não é válido perguntar a ValueA se é um, charjá que sua definição diz claramente que não é - isso é um erro de programação e eu gostaria que o compilador pegasse nisso. [Além disso, se eu pudesse fazer isso corretamente, então (espero) eu também obteria intellisense - o que seria uma bênção.]

Para conseguir isso, gostaria de dizer ao compilador que o tipo Tpode ser A, B ou 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;
    } 

Alguém tem ideia se o que quero alcançar é possível? Ou eu sou simplesmente estúpido por escrever esta classe em primeiro lugar?

Desde já, obrigado.


3
As uniões em C podem ser implementadas em C # para tipos de valor usando StructLayout(LayoutKind.Explicit)e FieldOffset. Isso não pode ser feito com tipos de referência, é claro. O que você está fazendo não é nada parecido com um C Union.
Brian

4
Isso geralmente é chamado de união discriminada .
Jeff Hardy

Obrigado Jeff - Eu não conhecia esse termo, mas isso é exatamente o que eu quero alcançar
Chris Fewtrell

7
Provavelmente não é o tipo de resposta que você está procurando, mas você considerou o F #? Ele tem uniões seguras de tipo e correspondência de padrões incorporadas diretamente na linguagem, muito mais fácil de representar as uniões do que com C #.
Julieta

1
Outro nome para a união discriminada é um tipo de soma.
cdiggins

Respostas:


113

Eu realmente não gosto das soluções de verificação de tipo e conversão de tipo fornecidas acima, então aqui está uma união 100% segura de tipo que lançará erros de compilação se você tentar usar o tipo de dados errado:

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);
            }
        }
    }
}

3
Sim, se você quer sindicatos discriminados e tipográficos, você vai precisar match, e essa é uma maneira tão boa de consegui-lo quanto qualquer outra.
Pavel Minaev

20
E se todo esse código clichê te deixar para baixo, você pode tentar esta implementação que marca explicitamente os casos: pastebin.com/EEdvVh2R . A propósito, esse estilo é muito semelhante ao modo como F # e OCaml representam os sindicatos internamente.
Julieta

4
Eu gosto do código mais curto de Juliet, mas e se os tipos forem <int, int, string>? Como você chamaria o segundo construtor?
Robert Jeppesen

2
Não sei como isso não tem 100 votos positivos. É uma questão de beleza!
Paolo Falabella

5
@nexus considera este tipo em F #:type Result = Success of int | Error of int
AlexFoxGill

33

Eu gosto da direção da solução aceita, mas ela não se ajusta bem para sindicatos de mais de três itens (por exemplo, uma união de 9 itens exigiria 9 definições de classe).

Aqui está outra abordagem que também é 100% segura em tempo de compilação, mas que é fácil de expandir para grandes sindicatos.

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));
    }
}

+1 Isso deve obter mais aprovações; Gosto da maneira como você o tornou flexível o suficiente para permitir sindicatos de todos os tipos de áreas.
Paul d'Aoust

+1 pela flexibilidade e brevidade de sua solução. No entanto, existem alguns detalhes que me incomodam. Vou postar cada um como um comentário separado:
stakx - não contribuindo mais em

1
1. O uso de reflexão pode incorrer em uma penalidade de desempenho muito grande em alguns cenários, visto que sindicatos discriminados, devido à sua natureza fundamental, podem ser usados ​​com muita frequência.
stakx - não contribuindo mais em

4
2. O uso de dynamic& genéricos em UnionBase<A>e a cadeia de herança parece desnecessário. Torne UnionBase<A>não genérico, elimine o construtor pegando um Ae faça valueum object(o que é de qualquer maneira; não há nenhum benefício adicional em declarar isso dynamic). Em seguida, derivar cada Union<…>classe diretamente de UnionBase. Isso tem a vantagem de que apenas o Match<T>(…)método adequado será exposto. (Como está agora, por exemplo, Union<A, B>expõe uma sobrecarga Match<T>(Func<A, T> fa)que com certeza lançará uma exceção se o valor incluído não for um A. Isso não deveria acontecer.)
stakx - não contribuindo mais em

3
Você pode achar minha biblioteca OneOf útil, ela faz mais ou menos isso, mas está no Nuget :) github.com/mcintyre321/OneOf
mcintyre321

20

Eu escrevi alguns posts sobre este assunto que podem ser úteis:

Digamos que você tenha um cenário de carrinho de compras com três estados: "Vazio", "Ativo" e "Pago", cada um com um comportamento diferente .

  • Você cria uma ICartStateinterface que todos os estados têm em comum (e poderia ser apenas uma interface de marcador vazia)
  • Você cria três classes que implementam essa interface. (As classes não precisam estar em um relacionamento de herança)
  • A interface contém um método "dobrado", por meio do qual você passa um lambda para cada estado ou caso que precisa tratar.

Você poderia usar o tempo de execução F # do C #, mas como uma alternativa mais leve, escrevi um pequeno modelo T4 para gerar código como este.

Esta é a interface:

partial interface ICartState
{
  ICartState Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        );
}

E aqui está a implementação:

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);
  }
}

Agora, digamos que você estenda o CartStateEmptye CartStateActivecom um AddItemmétodo que não é implementado por CartStatePaid.

E também vamos dizer que CartStateActivetem um Paymétodo que os outros estados não têm.

Então, aqui está um código que mostra isso em uso - adicionando dois itens e, em seguida, pagando pelo carrinho:

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
        );
}    

Observe que este código é completamente tipificado - sem conversão ou condicionais em lugar nenhum e erros de compilador se você tentar pagar por um carrinho vazio, por exemplo.


Caso de uso interessante. Para mim, implementar as uniões discriminadas nos próprios objetos é bastante prolixo. Aqui está uma alternativa de estilo funcional que usa expressões de alternância, com base em seu modelo: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Você pode ver que as DUs não são realmente necessárias se houver apenas um caminho "feliz", mas elas se tornam muito úteis quando um método pode retornar um tipo ou outro, dependendo das regras da lógica de negócios.
David Cuccia

12

Eu escrevi uma biblioteca para fazer isso em https://github.com/mcintyre321/OneOf

Instalar-Pacote OneOf

Ele contém os tipos genéricos para fazer DUs, por exemplo, OneOf<T0, T1>até OneOf<T0, ..., T9>. Cada um deles tem um .Match, e uma .Switchinstrução que você pode usar para comportamento seguro do compilador, por exemplo:

`` `

OneOf<string, ColorName, Color> backgroundColor = getBackground(); 
Color c = backgroundColor.Match(
    str => CssHelper.GetColorFromString(str),
    name => new Color(name),
    col => col
);

`` `


7

Não tenho certeza se entendi totalmente seu objetivo. Em C, uma união é uma estrutura que usa os mesmos locais de memória para mais de um campo. Por exemplo:

typedef union
{
    float real;
    int scalar;
} floatOrScalar;

A floatOrScalarunião pode ser usada como float ou int, mas ambos consomem o mesmo espaço de memória. Mudar um muda o outro. Você pode conseguir o mesmo com uma estrutura em C #:

[StructLayout(LayoutKind.Explicit)]
struct FloatOrScalar
{
    [FieldOffset(0)]
    public float Real;
    [FieldOffset(0)]
    public int Scalar;
}

A estrutura acima usa um total de 32 bits, em vez de 64 bits. Isso só é possível com uma estrutura. Seu exemplo acima é uma classe e, dada a natureza do CLR, não garante a eficiência da memória. Se você alterar um Union<A, B, C>de um tipo para outro, não estará necessariamente reutilizando memória ... muito provavelmente, você está alocando um novo tipo no heap e soltando um ponteiro diferente no objectcampo de apoio . Ao contrário de uma união real , sua abordagem pode realmente causar mais thrashing de heap do que você obteria se não usasse seu tipo Union.


Como mencionei em minha pergunta, minha motivação não era uma melhor eficiência de memória. Mudei o título da pergunta para refletir melhor qual é o meu objetivo - o título original de "união C (ish)" é, em retrospectiva, enganoso
Chris Fewtrell

Uma união discriminada faz muito mais sentido para o que você está tentando fazer. Quanto a torná-lo verificado em tempo de compilação ... Eu examinaria o .NET 4 e os Contratos de Código. Com os Contratos de código, pode ser possível impor um Contrato em tempo de compilação.Requires que imponha seus requisitos no operador .Is <T>.
Jrista

Acho que ainda tenho que questionar o uso de um Sindicato, na prática geral. Mesmo em C / C ++, as uniões são uma coisa arriscada e devem ser usadas com extremo cuidado. Estou curioso para saber por que você precisa trazer tal construção para C # ... qual o valor que você percebe obter com isso?
Jrista

2
char foo = 'B';

bool bar = foo is int;

Isso resulta em um aviso, não em um erro. Se você está procurando os seus Ise Asfunções a serem análogos para os operadores C #, então você não deveria estar restringindo-os dessa forma de qualquer maneira.


2

Se você permitir vários tipos, não poderá obter segurança de tipo (a menos que os tipos estejam relacionados).

Você não pode e não vai conseguir nenhum tipo de segurança de tipo, você só pode conseguir segurança de valor de byte usando FieldOffset.

Faria muito mais sentido ter um genérico ValueWrapper<T1, T2>com T1 ValueAe T2 ValueB, ...

PS: quando falo sobre segurança de tipo, quero dizer segurança de tipo em tempo de compilação.

Se você precisa de um wrapper de código (realizando lógica de negócios em modificações, você pode usar algo como:

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); }
}

Para uma saída fácil você pode usar (tem problemas de desempenho, mas é muito simples):

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

Sua sugestão de tornar o ValueWrapper genérico parece a resposta óbvia, mas me causa problemas no que estou fazendo. Essencialmente, meu código está criando esses objetos de invólucro analisando algumas linhas de texto. Portanto, tenho um método como ValueWrapper MakeValueWrapper (string text). Se eu tornar o wrapper genérico, preciso alterar a assinatura de MakeValueWrapper para ser genérica e isso, por sua vez, significa que o código de chamada precisa saber quais tipos são esperados e eu simplesmente não sei isso com antecedência antes de analisar o texto ...
Chris Fewtrell

... mas mesmo enquanto escrevia o último comentário, parecia que talvez tivesse perdido algo (ou bagunçado algo) porque o que estou tentando fazer não parece como deveria ser tão difícil quanto estou fazendo. Acho que vou voltar e passar alguns minutos trabalhando em um wrapper generificado e ver se consigo adaptar o código de análise em torno dele.
Chris Fewtrell

O código que forneci deve ser apenas para lógica de negócios. O problema com a sua abordagem é que você nunca sabe qual valor é armazenado no Union em tempo de compilação. Isso significa que você terá que usar as instruções if ou switch sempre que acessar o objeto Union, uma vez que esses objetos não compartilham uma funcionalidade comum! Como você vai usar os objetos de invólucro posteriormente em seu código? Além disso, você pode construir objetos genéricos em tempo de execução (lento, mas possível). Outra opção fácil com está no meu post editado.
Jaroslav Jandek

Você basicamente não tem verificações de tipo de tempo de compilação significativas em seu código agora - você também pode tentar objetos dinâmicos (verificação de tipo dinâmico em tempo de execução).
Jaroslav Jandek

2

Aqui está minha tentativa. Ele compila a verificação de tipos, usando restrições de tipo genéricas.

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>()
    }
}

Ele poderia usar um pouco de embelezamento. Especialmente, não consegui descobrir como me livrar dos parâmetros de tipo para As / Is / Set (não há uma maneira de especificar um parâmetro de tipo e deixar C # descobrir o outro?)


2

Já tive esse mesmo problema muitas vezes e acabei de chegar a uma solução que obtém a sintaxe que desejo (às custas de alguma feiura na implementação do tipo Union).

Para recapitular: queremos esse tipo de uso no site da chamada.

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";

Queremos que os exemplos a seguir falhem na compilação, no entanto, para obtermos um mínimo de segurança de tipo.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Para crédito extra, também não vamos ocupar mais espaço do que o absolutamente necessário.

Com tudo isso dito, aqui está minha implementação para dois parâmetros de tipo genérico. A implementação para três, quatro e assim por diante parâmetros de tipo é direta.

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();
        }
    }
}

2

E minha tentativa de solução mínima, mas extensível, usando aninhamento de tipo Union / Either . Além disso, o uso de parâmetros padrão no método Match habilita naturalmente o cenário "X ou padrão".

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);
            }
        }
    }
}

1

Você pode lançar exceções quando houver uma tentativa de acessar variáveis ​​que não foram inicializadas, ou seja, se for criado com um parâmetro A e posteriormente houver uma tentativa de acessar B ou C, pode lançar, digamos, UnsupportedOperationException. Você precisaria de um getter para fazer funcionar.


Sim - a primeira versão que escrevi levantou uma exceção no método As - mas embora isso certamente destaque o problema no código, prefiro muito mais ser informado sobre isso em tempo de compilação do que em tempo de execução.
Chris Fewtrell

0

Você pode exportar uma função de correspondência de pseudo-padrão, como eu uso para o tipo Either na minha biblioteca Sasa . Atualmente, há sobrecarga de tempo de execução, mas pretendo adicionar uma análise CIL para incorporar todos os delegados em uma instrução de caso verdadeiro.


0

Não é possível fazer exatamente com a sintaxe que você usou, mas com um pouco mais de detalhamento e copiar / colar é fácil fazer com que a resolução de sobrecarga faça o trabalho para você:


// 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());
}

Agora deve ser bastante óbvio como implementá-lo:


    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
    {
    }

Não há verificações para extrair o valor do tipo errado, por exemplo:


var u = Union(10);
string s = u.Value(Get.ForType());

Portanto, você pode considerar adicionar verificações necessárias e lançar exceções em tais casos.


0

Eu uso o próprio do tipo de união.

Considere um exemplo para torná-lo mais claro.

Imagine que temos aula de contato:

public class Contact 
{
    public string Name { get; set; }
    public string EmailAddress { get; set; }
    public string PostalAdrress { get; set; }
}

Todos são definidos como strings simples, mas realmente são apenas strings? Claro que não. O nome pode consistir em nome e sobrenome. Ou um e-mail é apenas um conjunto de símbolos? Eu sei que pelo menos deve conter @ e é necessariamente.

Vamos melhorar nosso modelo de domínio

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) { ... } 
}

Nestas aulas haverá validações durante a criação e, eventualmente, teremos modelos válidos. Consturctor na classe PersonaName requer FirstName e LastName ao mesmo tempo. Isso significa que após a criação, ele não pode ter estado inválido.

E classe de contato respectivamente

public class Contact 
{
    public PersonalName Name { get; set; }
    public EmailAdress EmailAddress { get; set; }
    public PostalAddress PostalAddress { get; set; }
}

Neste caso temos o mesmo problema, o objeto da classe Contact pode estar em estado inválido. Quer dizer, pode ter EmailAddress, mas não tem nome

var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };

Vamos corrigi-lo e criar a classe Contact com construtor que requer PersonalName, EmailAddress e PostalAddress:

public class Contact 
{
    public Contact(
               PersonalName personalName, 
               EmailAddress emailAddress,
               PostalAddress postalAddress
           ) 
    { 
         ... 
    }
}

Mas aqui temos outro problema. E se Person tiver apenas EmailAdress e não PostalAddress?

Se pensarmos nisso, percebemos que existem três possibilidades de estado válido do objeto da classe Contact:

  1. Um contato tem apenas um endereço de e-mail
  2. Um contato tem apenas um endereço postal
  3. Um contato possui um endereço de e-mail e um endereço postal

Vamos escrever modelos de domínio. Para o início, criaremos a classe Contact Info cujo estado será correspondente aos casos acima.

public class ContactInfo 
{
    public ContactInfo(EmailAddress emailAddress) { ... }
    public ContactInfo(PostalAddress postalAddress) { ... }
    public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... }
}

E classe de contato:

public class Contact 
{
    public Contact(
              PersonalName personalName,
              ContactInfo contactInfo
           )
    {
        ...
    }
}

Vamos tentar usar:

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

Vamos adicionar o método Match na classe 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)
        {
             ...
        } 
        ...
   }
}

No método match, podemos escrever este código, pois o estado da classe de contato é controlado com construtores e pode ter apenas um dos estados possíveis.

Vamos criar uma classe auxiliar, para que a cada vez não escrevamos tantos códigos.

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");
    }
}

Podemos ter essa classe com antecedência para vários tipos, como é feito com os delegados Func, Action. 4-6 parâmetros de tipo genérico estarão completos para a classe Union.

Vamos reescrever a ContactInfoaula:

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) { }
}

Aqui, o compilador solicitará a substituição de pelo menos um construtor. Se esquecermos de substituir o resto dos construtores, não podemos criar o objeto da classe ContactInfo com outro estado. Isso nos protegerá de exceções de tempo de execução durante o Matching.

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()
            )
    );

Isso é tudo. Eu espero que você tenha gostado.

Exemplo retirado do site F # para diversão e lucro


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.