Contrainte de type multiple (OR) de méthode générique

135

En lisant ceci , j'ai appris qu'il était possible d'autoriser une méthode à accepter des paramètres de plusieurs types en en faisant une méthode générique. Dans l'exemple, le code suivant est utilisé avec une contrainte de type pour s'assurer que "U" est un IEnumerable<T>.

public T DoSomething<U, T>(U arg) where U : IEnumerable<T>
{
    return arg.First();
}

J'ai trouvé un peu plus de code qui permettait d'ajouter plusieurs contraintes de type, telles que:

public void test<T>(string a, T arg) where T: ParentClass, ChildClass 
{
    //do something
}

Cependant, ce code semble appliquer qui argdoit être à la fois un type de ParentClass et ChildClass . Ce que je veux faire, c'est dire que arg pourrait être un type de ParentClass ou ChildClass de la manière suivante:

public void test<T>(string a, T arg) where T: string OR Exception
{
//do something
}

Votre aide est appréciée comme toujours!

Mansfield
la source
4
Que pourriez-vous faire utilement, de manière générique dans le corps d'une telle méthode (à moins que les types multiples ne dérivent tous d'une classe de base spécifique, auquel cas pourquoi ne pas déclarer cela comme contrainte de type)?
Damien_The_Unbeliever
@Damien_The_Unbeliever Vous ne savez pas ce que vous entendez par le corps? Sauf si vous voulez dire autoriser n'importe quel type et vérifier manuellement le corps ... et dans le code réel que je veux écrire (le dernier extrait de code), j'aimerais pouvoir passer une chaîne OU une exception, donc il n'y a pas de classe relation là-bas (sauf system.object j'imagine).
Mansfield
1
Notez également que l'écriture ne sert à rien where T : string, tout comme stringune classe scellée . La seule chose utile que vous puissiez faire est de définir des surcharges pour stringet T : Exception, comme expliqué par @ Botz3000 dans sa réponse ci-dessous.
Mattias Buelens
Mais quand il n'y a pas de relation, les seules méthodes auxquelles vous pouvez faire appel argsont celles définies par object- alors pourquoi ne pas simplement supprimer les génériques de l'image et en faire le type arg object? Que gagnez-vous?
Damien_The_Unbeliever
1
@Mansfield Vous pouvez créer une méthode privée qui accepte un paramètre d'objet. Les deux surcharges appelleraient ça. Aucun générique nécessaire ici.
Botz3000

Réponses:

68

Ce n'est pas possible. Vous pouvez cependant définir des surcharges pour des types spécifiques:

public void test(string a, string arg);
public void test(string a, Exception arg);

Si ceux-ci font partie d'une classe générique, ils seront préférés à la version générique de la méthode.

Botz3000
la source
1
Intéressant. Cela m'est venu à l'esprit, mais j'ai pensé que cela pourrait rendre le code plus propre en utilisant une fonction. Ah bien, merci beaucoup! Juste par curiosité, savez-vous s'il y a une raison spécifique pour laquelle ce n'est pas possible? (a-t-il été délibérément laissé hors de la langue?)
Mansfield
3
@Mansfield Je ne connais pas la raison exacte, mais je pense que vous ne pourriez plus utiliser les paramètres génériques de manière significative. À l'intérieur de la classe, ils devraient être traités comme des objets s'ils étaient autorisés à être de types complètement différents. Cela signifie que vous pouvez tout aussi bien omettre le paramètre générique et fournir des surcharges.
Botz3000
3
@Mansfield, c'est parce qu'une orrelation rend les choses trop générales pour être utiles. Vous souhaitez avoir à faire une réflexion pour savoir ce qu'il faut faire et tout ça. (Beurk!).
Chris Pfohl
29

La réponse de Botz est correcte à 100%, voici une courte explication:

Lorsque vous écrivez une méthode (générique ou non) et déclarez les types des paramètres que la méthode prend, vous définissez un contrat:

Si vous me donnez un objet qui sait comment faire l'ensemble des choses que le Type T sait faire, je peux fournir soit 'a': une valeur de retour du type que je déclare, soit 'b': une sorte de comportement qui utilise ce type.

Si vous essayez de lui donner plus d'un type à la fois (en ayant un ou) ou essayez de le faire renvoyer une valeur qui pourrait être plus d'un type, ce contrat devient flou:

Si vous me donnez un objet qui sait sauter à la corde ou qui sait calculer pi au 15e chiffre, je retournerai soit un objet qui peut aller à la pêche, soit peut-être mélanger du béton.

Le problème est que lorsque vous entrez dans la méthode, vous ne savez pas s'ils vous ont donné un IJumpRopeou un PiFactory. De plus, lorsque vous utilisez la méthode (en supposant que vous l'avez compilée par magie), vous ne savez pas vraiment si vous avez un Fisherou unAbstractConcreteMixer . Fondamentalement, cela rend le tout beaucoup plus déroutant.

La solution à votre problème est l'une des deux possibilités:

  1. Définissez plusieurs méthodes qui définissent chaque transformation, comportement ou autre. C'est la réponse de Botz. Dans le monde de la programmation, on parle de surcharge de la méthode.

  2. Définissez une classe ou une interface de base qui sait comment faire tout ce dont vous avez besoin pour la méthode et une seule méthode prend juste ce type. Cela peut impliquer de regrouper un stringet Exceptiondans une petite classe pour définir comment vous prévoyez de les mapper à l'implémentation, mais tout est alors très clair et facile à lire. Je pourrais venir, dans quatre ans, lire votre code et comprendre facilement ce qui se passe.

Ce que vous choisissez dépend de la complexité des choix 1 et 2 et de leur capacité d'extension.

Donc, pour votre situation spécifique, je vais imaginer que vous sortez simplement un message ou quelque chose de l'exception:

public interface IHasMessage
{
    string GetMessage();
}

public void test(string a, IHasMessage arg)
{
    //Use message
}

Maintenant, tout ce dont vous avez besoin, ce sont des méthodes qui transforment a stringet an Exceptionen IHasMessage. Très facile.

Chris Pfohl
la source
désolé @ Botz3000, je viens de remarquer que j'ai mal orthographié votre nom.
Chris Pfohl
Ou le compilateur pourrait menacer le type en tant que type d'union dans la fonction et que la valeur de retour soit du même type que celle dans laquelle il entre. TypeScript fait cela.
Alex
@Alex mais ce n'est pas ce que fait C #.
Chris Pfohl
C'est vrai, mais c'est possible. J'ai lu cette réponse comme étant impossible, ai-je mal interprété?
Alex
1
Le fait est que les contraintes de paramètres génériques C # et les génériques eux-mêmes sont assez primitifs par rapport, par exemple, aux modèles C ++. C # vous oblige à indiquer au compilateur à l'avance les opérations autorisées sur les types génériques. La façon de fournir ces informations est d'ajouter une contrainte d'interface implémente (où T: IDisposable). Mais vous ne souhaitez peut-être pas que votre type implémente une interface pour utiliser une méthode générique ou vous pouvez autoriser certains types dans votre code générique qui n'ont pas d'interface commune. Ex. autorisez n'importe quelle structure ou chaîne afin que vous puissiez simplement appeler Equals (v1, v2) pour une comparaison basée sur des valeurs.
Vakhtang
8

Si ChildClass signifie qu'il est dérivé de ParentClass, vous pouvez simplement écrire ce qui suit pour accepter à la fois ParentClass et ChildClass;

public void test<T>(string a, T arg) where T: ParentClass 
{
    //do something
}

D'un autre côté, si vous souhaitez utiliser deux types différents sans relation d'héritage entre eux, vous devez considérer les types implémentant la même interface;

public interface ICommonInterface
{
    string SomeCommonProperty { get; set; }
}

public class AA : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;set;
    }
}

public class BB : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;
        set;
    }
}

alors vous pouvez écrire votre fonction générique comme;

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}
daryal
la source
Bonne idée, sauf que je ne pense pas que je serai capable de le faire car je veux utiliser une chaîne qui est une classe scellée selon un commentaire ci-dessus ...
Mansfield
c'est un problème de conception en fait. une fonction générique est utilisée pour effectuer des opérations similaires avec la réutilisation de code. les deux si vous prévoyez de faire différentes opérations dans le corps de la méthode, séparer les méthodes est une meilleure façon (IMHO).
daryal
Ce que je fais, en fait, c'est écrire une simple fonction de journalisation des erreurs. Je voudrais que ce dernier paramètre soit une chaîne d'informations sur l'erreur, ou une exception, auquel cas j'enregistre quand même e.message + e.stacktrace sous forme de chaîne.
Mansfield
vous pouvez écrire une nouvelle classe, ayant isSuccesful, enregistrer le message et l'exception en tant que propriétés. alors vous pouvez vérifier si issueccesful true et faire le reste.
13 h 24
1

Aussi vieille que cette question soit, je reçois toujours des votes positifs aléatoires sur mon explication ci-dessus. L'explication tient toujours parfaitement bien telle quelle, mais je vais répondre une deuxième fois avec un type qui m'a bien servi en tant que substitut aux types d'union (la réponse fortement typée à la question qui n'est pas directement prise en charge par C # tel quel ).

using System;
using System.Diagnostics;

namespace Union {
    [DebuggerDisplay("{currType}: {ToString()}")]
    public struct Either<TP, TA> {
        enum CurrType {
            Neither = 0,
            Primary,
            Alternate,
        }
        private readonly CurrType currType;
        private readonly TP primary;
        private readonly TA alternate;

        public bool IsNeither => currType == CurrType.Primary;
        public bool IsPrimary => currType == CurrType.Primary;
        public bool IsAlternate => currType == CurrType.Alternate;

        public static implicit operator Either<TP, TA>(TP val) => new Either<TP, TA>(val);

        public static implicit operator Either<TP, TA>(TA val) => new Either<TP, TA>(val);

        public static implicit operator TP(Either<TP, TA> @this) => @this.Primary;

        public static implicit operator TA(Either<TP, TA> @this) => @this.Alternate;

        public override string ToString() {
            string description = IsNeither ? "" :
                $": {(IsPrimary ? typeof(TP).Name : typeof(TA).Name)}";
            return $"{currType.ToString("")}{description}";
        }

        public Either(TP val) {
            currType = CurrType.Primary;
            primary = val;
            alternate = default(TA);
        }

        public Either(TA val) {
            currType = CurrType.Alternate;
            alternate = val;
            primary = default(TP);
        }

        public TP Primary {
            get {
                Validate(CurrType.Primary);
                return primary;
            }
        }

        public TA Alternate {
            get {
                Validate(CurrType.Alternate);
                return alternate;
            }
        }

        private void Validate(CurrType desiredType) {
            if (desiredType != currType) {
                throw new InvalidOperationException($"Attempting to get {desiredType} when {currType} is set");
            }
        }
    }
}

La classe ci - dessus représente un type qui peut être soit TP ou TA. Vous pouvez l'utiliser comme tel (les types renvoient à ma réponse d'origine):

// ...
public static Either<FishingBot, ConcreteMixer> DemoFunc(Either<JumpRope, PiCalculator> arg) {
  if (arg.IsPrimary) {
    return new FishingBot(arg.Primary);
  }
  return new ConcreteMixer(arg.Secondary);
}

// elsewhere:

var fishBotOrConcreteMixer = DemoFunc(new JumpRope());
var fishBotOrConcreteMixer = DemoFunc(new PiCalculator());

Notes IMPORTANTES:

  • Vous obtiendrez des erreurs d'exécution si vous ne cochez pas IsPrimary abord.
  • Vous pouvez vérifier l'un des fichiers IsNeither IsPrimaryou IsAlternate.
  • Vous pouvez accéder à la valeur via PrimaryetAlternate
  • Il existe des convertisseurs implicites entre TP / TA et Either pour vous permettre de transmettre soit les valeurs, soit un Eithern'importe où où l'on est attendu. Si vous faites passer un Eitheroù un TAou TPdevrait, mais Eithercontient le mauvais type de valeur que vous obtiendrez une erreur d'exécution.

J'utilise généralement ceci où je veux qu'une méthode renvoie un résultat ou une erreur. Cela nettoie vraiment ce code de style. J'utilise aussi très occasionnellement ( rarement ) ceci en remplacement des surcharges de méthode. En réalité, c'est un très mauvais substitut à une telle surcharge.

Chris Pfohl
la source