[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 ValueA
só podem ser de alguns tipos (digamos string
, int
e Foo
(que é uma classe) e ValueB
podem 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 Union
porque 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, char
já 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 T
pode 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.
fonte
StructLayout(LayoutKind.Explicit)
eFieldOffset
. 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.Respostas:
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:
fonte
match
, e essa é uma maneira tão boa de consegui-lo quanto qualquer outra.type Result = Success of int | Error of int
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.
fonte
dynamic
& genéricos emUnionBase<A>
e a cadeia de herança parece desnecessário. TorneUnionBase<A>
não genérico, elimine o construtor pegando umA
e façavalue
umobject
(o que é de qualquer maneira; não há nenhum benefício adicional em declarar issodynamic
). Em seguida, derivar cadaUnion<…>
classe diretamente deUnionBase
. Isso tem a vantagem de que apenas oMatch<T>(…)
método adequado será exposto. (Como está agora, por exemplo,Union<A, B>
expõe uma sobrecargaMatch<T>(Func<A, T> fa)
que com certeza lançará uma exceção se o valor incluído não for umA
. Isso não deveria acontecer.)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 .
ICartState
interface que todos os estados têm em comum (e poderia ser apenas uma interface de marcador vazia)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:
E aqui está a implementação:
Agora, digamos que você estenda o
CartStateEmpty
eCartStateActive
com umAddItem
método que não é implementado porCartStatePaid
.E também vamos dizer que
CartStateActive
tem umPay
mé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:
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.
fonte
Eu escrevi uma biblioteca para fazer isso em https://github.com/mcintyre321/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.Switch
instrução que você pode usar para comportamento seguro do compilador, por exemplo:`` `
`` `
fonte
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:
A
floatOrScalar
uniã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 #: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 noobject
campo 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.fonte
Isso resulta em um aviso, não em um erro. Se você está procurando os seus
Is
eAs
funções a serem análogos para os operadores C #, então você não deveria estar restringindo-os dessa forma de qualquer maneira.fonte
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>
comT1 ValueA
eT2 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:
Para uma saída fácil você pode usar (tem problemas de desempenho, mas é muito simples):
fonte
Aqui está minha tentativa. Ele compila a verificação de tipos, usando restrições de tipo genéricas.
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?)
fonte
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.
Queremos que os exemplos a seguir falhem na compilação, no entanto, para obtermos um mínimo de segurança de tipo.
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.
fonte
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".
fonte
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.
fonte
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.
fonte
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ê:
Agora deve ser bastante óbvio como implementá-lo:
Não há verificações para extrair o valor do tipo errado, por exemplo:
Portanto, você pode considerar adicionar verificações necessárias e lançar exceções em tais casos.
fonte
Eu uso o próprio do tipo de união.
Considere um exemplo para torná-lo mais claro.
Imagine que temos aula de contato:
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
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
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
Vamos corrigi-lo e criar a classe Contact com construtor que requer PersonalName, EmailAddress e 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:
Vamos escrever modelos de domínio. Para o início, criaremos a classe Contact Info cujo estado será correspondente aos casos acima.
E classe de contato:
Vamos tentar usar:
Vamos adicionar o método Match na classe ContactInfo
Vamos criar uma classe auxiliar, para que a cada vez não escrevamos tantos códigos.
Vamos reescrever a
ContactInfo
aula:Isso é tudo. Eu espero que você tenha gostado.
Exemplo retirado do site F # para diversão e lucro
fonte
A equipe de design da linguagem C # discutiu as uniões discriminadas em janeiro de 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Você pode votar na solicitação de recurso em https://github.com/dotnet/csharplang/issues/113
fonte