[Note: Cette question avait le titre original " Union de style C (ish) en C # " mais comme le commentaire de Jeff m'a informé, apparemment cette structure s'appelle une "union discriminée"]
Excusez la verbosité de cette question.
Il y a quelques questions similaires aux miennes déjà dans SO, mais elles semblent se concentrer sur les avantages d'économie de mémoire du syndicat ou de l'utiliser pour l'interopérabilité. Voici un exemple d'une telle question .
Mon désir d'avoir un truc de type syndical est quelque peu différent.
J'écris actuellement du code qui génère des objets qui ressemblent un peu à ça
public class ValueWrapper
{
public DateTime ValueCreationDate;
// ... other meta data about the value
public object ValueA;
public object ValueB;
}
Des trucs assez compliqués, je pense que vous serez d'accord. Le fait est que ValueA
cela ne peut être que de certains types (disons string
, int
et Foo
(qui est une classe) et ValueB
peut être un autre petit ensemble de types. Je n'aime pas traiter ces valeurs comme des objets (je veux la sensation chaleureuse de codage avec un peu de sécurité de type).
J'ai donc pensé à écrire une petite classe wrapper triviale pour exprimer le fait que ValueA est logiquement une référence à un type particulier. J'ai appelé la classe Union
parce que ce que j'essaie d'accomplir m'a rappelé le concept d'union en 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);
}
}
Utiliser cette classe ValueWrapper ressemble maintenant à ceci
public class ValueWrapper2
{
public DateTime ValueCreationDate;
public Union<int, string, Foo> ValueA;
public Union<double, Bar, Foo> ValueB;
}
qui est quelque chose comme ce que je voulais réaliser mais il me manque un élément assez crucial - c'est la vérification de type appliquée par le compilateur lors de l'appel des fonctions Is et As, comme le montre le code suivant
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 Il n'est pas valide de demander à ValueA si c'est un char
car sa définition dit clairement que ce n'est pas le cas - c'est une erreur de programmation et j'aimerais que le compilateur s'en aperçoive. [Aussi, si je pouvais obtenir cela correctement, alors (j'espère) j'obtiendrais aussi de l'intellisense - ce qui serait une aubaine.]
Pour ce faire, je voudrais dire au compilateur que le type T
peut être l'un de 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;
}
Quelqu'un a-t-il une idée si ce que je veux réaliser est possible? Ou suis-je tout simplement stupide pour avoir écrit ce cours en premier lieu?
Merci d'avance.
la source
StructLayout(LayoutKind.Explicit)
etFieldOffset
. Cela ne peut pas être fait avec les types de référence, bien sûr. Ce que vous faites n'est pas du tout un syndicat C.Réponses:
Je n'aime pas vraiment les solutions de vérification de type et de conversion de type fournies ci-dessus, alors voici l'union de type 100% sûre qui provoquera des erreurs de compilation si vous essayez d'utiliser le mauvais type de données:
la source
match
, et c'est un aussi bon moyen de l'obtenir que n'importe quel autre.type Result = Success of int | Error of int
J'aime la direction de la solution acceptée, mais elle ne s'adapte pas bien aux unions de plus de trois items (par exemple, une union de 9 items exigerait 9 définitions de classe).
Voici une autre approche qui est également sécurisée à 100% au moment de la compilation, mais qui est facile à transformer en grandes unions.
la source
dynamic
& génériques dansUnionBase<A>
et la chaîne d'héritage semble inutile. RendreUnionBase<A>
non générique, tuer le constructeur prenant unA
, et créervalue
unobject
(ce qui est de toute façon; il n'y a aucun avantage supplémentaire à le déclarerdynamic
). Puis dérivez chaqueUnion<…>
classe directement à partir deUnionBase
. Cela présente l'avantage que seule laMatch<T>(…)
méthode appropriée sera exposée. (Tel qu'il est maintenant, par exemple,Union<A, B>
expose une surchargeMatch<T>(Func<A, T> fa)
qui est garantie de lever une exception si la valeur incluse n'est pas unA
. Cela ne devrait pas arriver.)J'ai écrit quelques articles de blog sur ce sujet qui pourraient être utiles:
Supposons que vous ayez un scénario de panier avec trois états: "Vide", "Actif" et "Payé", chacun avec un comportement différent .
ICartState
interface que tous les états ont en commun (et cela pourrait simplement être une interface de marqueur vide)Vous pouvez utiliser le runtime F # à partir de C #, mais comme alternative plus légère, j'ai écrit un petit modèle T4 pour générer du code comme celui-ci.
Voici l'interface:
Et voici la mise en œuvre:
Disons maintenant que vous étendez le
CartStateEmpty
etCartStateActive
avec uneAddItem
méthode qui n'est pas implémentée parCartStatePaid
.Et disons aussi que cela
CartStateActive
a unPay
méthode que les autres États n'ont pas.Ensuite, voici un code qui le montre en cours d'utilisation - en ajoutant deux articles puis en payant le panier:
Notez que ce code est complètement sûr de type - aucun casting ou conditionnel nulle part, et des erreurs de compilation si vous essayez de payer pour un panier vide, par exemple.
la source
J'ai écrit une bibliothèque pour faire cela à https://github.com/mcintyre321/OneOf
Il contient les types génériques pour faire des DU, par exemple
OneOf<T0, T1>
jusqu'àOneOf<T0, ..., T9>
. Chacun de ceux-ci a un.Match
, et une.Switch
instruction que vous pouvez utiliser pour un comportement typé sûr du compilateur, par exemple:''
''
la source
Je ne suis pas sûr de bien comprendre votre objectif. En C, une union est une structure qui utilise les mêmes emplacements mémoire pour plus d'un champ. Par exemple:
L'
floatOrScalar
union peut être utilisée comme un float ou un int, mais ils consomment tous les deux le même espace mémoire. Changer l'un change l'autre. Vous pouvez réaliser la même chose avec une structure en C #:La structure ci-dessus utilise 32 bits au total, plutôt que 64 bits. Ceci n'est possible qu'avec une structure. Votre exemple ci-dessus est une classe, et étant donné la nature du CLR, ne donne aucune garantie sur l'efficacité de la mémoire. Si vous changez un
Union<A, B, C>
d'un type à un autre, vous ne réutilisez pas nécessairement la mémoire ... très probablement, vous allouez un nouveau type sur le tas et déposez un pointeur différent dans leobject
champ de sauvegarde . Contrairement à une véritable union , votre approche peut en fait provoquer plus de débordements de tas que vous n'en auriez autrement si vous n'utilisiez pas votre type Union.la source
Cela entraîne un avertissement, pas une erreur. Si vous cherchez que vos fonctions
Is
etAs
soient des analogues pour les opérateurs C #, vous ne devriez pas les restreindre de cette façon de toute façon.la source
Si vous autorisez plusieurs types, vous ne pouvez pas atteindre la sécurité de type (sauf si les types sont liés).
Vous ne pouvez pas et n'obtiendrez aucun type de sécurité de type, vous ne pouvez obtenir une sécurité de valeur d'octet qu'en utilisant FieldOffset.
Il serait beaucoup plus logique d'avoir un générique
ValueWrapper<T1, T2>
avecT1 ValueA
etT2 ValueB
, ...PS: quand je parle de sécurité de type, je veux dire de sécurité de type au moment de la compilation.
Si vous avez besoin d'un wrapper de code (exécutant une logique commerciale sur les modifications, vous pouvez utiliser quelque chose du type:
Pour une solution simple, vous pouvez utiliser (il a des problèmes de performances, mais c'est très simple):
la source
Voici ma tentative. Il compile la vérification temporelle des types, en utilisant des contraintes de type génériques.
Cela pourrait nécessiter quelques jolies. Surtout, je ne pouvais pas comprendre comment se débarrasser des paramètres de type dans As / Is / Set (n'y a-t-il pas un moyen de spécifier un paramètre de type et de laisser C # figurer l'autre?)
la source
J'ai donc rencontré le même problème plusieurs fois et je viens de trouver une solution qui obtient la syntaxe que je veux (au détriment d'une certaine laideur dans la mise en œuvre du type Union.)
Pour récapituler: nous voulons ce type d'utilisation sur le site d'appel.
Nous voulons que les exemples suivants ne soient pas compilés, cependant, afin que nous obtenions un minimum de sécurité de type.
Pour un crédit supplémentaire, n'occupons pas non plus plus d'espace que nécessaire.
Cela dit, voici mon implémentation pour deux paramètres de type générique. L'implémentation des paramètres de type trois, quatre et ainsi de suite est simple.
la source
Et ma tentative de solution minimale mais extensible utilisant l' imbrication de type Union / Either . De plus, l'utilisation des paramètres par défaut dans la méthode Match active naturellement le scénario «Soit X soit par défaut».
la source
Vous pouvez lancer des exceptions une fois qu'il y a une tentative d'accès à des variables qui n'ont pas été initialisées, c'est-à-dire que s'il est créé avec un paramètre A et que plus tard, il y a une tentative d'accès à B ou C, il peut lancer, par exemple, une exception UnsupportedOperationException. Vous auriez besoin d'un getter pour le faire fonctionner.
la source
Vous pouvez exporter une fonction de correspondance de pseudo-motifs, comme je l'utilise pour le type Either dans ma bibliothèque Sasa . Il y a actuellement une surcharge d'exécution, mais je prévois éventuellement d'ajouter une analyse CIL pour intégrer tous les délégués dans une véritable déclaration de cas.
la source
Il n'est pas possible de faire exactement la syntaxe que vous avez utilisée, mais avec un peu plus de verbosité et de copier / coller, il est facile de faire en sorte que la résolution de surcharge fasse le travail pour vous:
À présent, il devrait être assez évident comment l'implémenter:
Il n'y a pas de contrôle pour extraire la valeur du mauvais type, par exemple:
Vous pouvez donc envisager d'ajouter les vérifications nécessaires et de lever des exceptions dans de tels cas.
la source
J'utilise le propre de Union Type.
Prenons un exemple pour le rendre plus clair.
Imaginez que nous ayons la classe Contact:
Celles-ci sont toutes définies comme de simples chaînes, mais ne sont-elles vraiment que des chaînes? Bien sûr que non. Le nom peut être composé du prénom et du nom. Ou un e-mail n'est-il qu'un ensemble de symboles? Je sais qu'au moins il devrait contenir @ et c'est nécessairement le cas.
Améliorons-nous le modèle de domaine
Dans ces classes, il y aura des validations lors de la création et nous aurons éventuellement des modèles valides. Le consturctor de la classe PersonaName requiert le prénom et le nom en même temps. Cela signifie qu'après la création, il ne peut pas avoir un état invalide.
Et classe de contact respectivement
Dans ce cas, nous avons le même problème, l'objet de la classe Contact peut être dans un état invalide. Je veux dire qu'il peut avoir EmailAddress mais pas Name
Corrigeons le problème et créons la classe Contact avec le constructeur qui nécessite PersonalName, EmailAddress et PostalAddress:
Mais ici, nous avons un autre problème. Que faire si la personne n'a que EmailAdress et n'a pas PostalAddress?
Si nous y réfléchissons, nous nous rendons compte qu'il existe trois possibilités d'état valide de l'objet de classe Contact:
Écrivons des modèles de domaine. Pour le début, nous allons créer la classe Contact Info dont l'état correspondra aux cas ci-dessus.
Et classe de contact:
Essayons de l'utiliser:
Ajoutons la méthode Match dans la classe ContactInfo
Créons une classe auxiliaire, pour qu'à chaque fois n'écrivez pas autant de code.
Réécrivons la
ContactInfo
classe:C'est tout. J'éspère que tu as apprécié.
Exemple tiré du site F # pour le plaisir et le profit
la source
L'équipe de conception du langage C # a discuté des syndicats discriminés en janvier 2017 https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md#discriminated-unions-via-closed-types
Vous pouvez voter pour la demande de fonctionnalité sur https://github.com/dotnet/csharplang/issues/113
la source