Einschränkung des generischen Methoden-Multiple-Typs (OR)

135

Als ich dies las , erfuhr ich, dass es möglich war, einer Methode zu erlauben, Parameter mehrerer Typen zu akzeptieren, indem sie zu einer generischen Methode gemacht wurde. Im Beispiel wird der folgende Code mit einer Typbeschränkung verwendet, um sicherzustellen, dass "U" ein ist IEnumerable<T>.

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

Ich habe mehr Code gefunden, mit dem mehrere Typeinschränkungen hinzugefügt werden können, z.

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

Dieser Code scheint jedoch zu erzwingen, dass argdies sowohl eine Art von ParentClass als auch sein muss ChildClass . Was ich tun möchte, ist zu sagen, dass arg eine Art ParentClass oder ChildClass auf folgende Weise sein könnte:

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

Ihre Hilfe wird wie immer geschätzt!

Mansfield
quelle
4
Was können Sie im Hauptteil einer solchen Methode generisch sinnvoll tun (es sei denn, die mehreren Typen stammen alle von einer bestimmten Basisklasse. In diesem Fall sollten Sie dies als Typbeschränkung deklarieren).
Damien_The_Unbeliever
@Damien_The_Unbeliever Nicht sicher, was du mit im Körper meinst? Wenn Sie nicht meinen, einen Typ zuzulassen und den Text manuell zu überprüfen ... und den tatsächlichen Code, den ich schreiben möchte (das letzte Code-Snippet), möchte ich in der Lage sein, eine Zeichenfolge ODER eine Ausnahme zu übergeben, sodass es keine Klasse gibt Beziehung dort (außer system.object stelle ich mir vor).
Mansfield
1
Beachten Sie auch, dass das Schreiben nicht sinnvoll ist where T : string, ebenso stringwie eine versiegelte Klasse. Das einzig Nützliche, was Sie tun können, ist das Definieren von Überladungen für stringund T : Exception, wie von @ Botz3000 in seiner Antwort unten erläutert.
Mattias Buelens
Aber wenn es keine Beziehung gibt, können Sie nur die Methoden aufrufen, argdie durch definiert sind. objectWarum also nicht einfach Generika aus dem Bild entfernen und den Typ festlegen arg object? Was gewinnen Sie?
Damien_The_Unbeliever
1
@ Mansfield Sie können eine private Methode erstellen , die einen Objektparameter akzeptiert. Beide Überladungen würden das nennen. Hier werden keine Generika benötigt.
Botz3000

Antworten:

68

Das ist nicht möglich. Sie können jedoch Überladungen für bestimmte Typen definieren:

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

Wenn diese Teil einer generischen Klasse sind, werden sie der generischen Version der Methode vorgezogen.

Botz3000
quelle
1
Interessant. Dies war mir eingefallen, aber ich dachte, es könnte den Code sauberer machen, eine Funktion zu verwenden. Na ja, vielen Dank! Wissen Sie aus Neugier, ob es einen bestimmten Grund gibt, warum dies nicht möglich ist? (Wurde es absichtlich aus der Sprache herausgelassen?)
Mansfield
3
@ Mansfield Ich kenne den genauen Grund nicht, aber ich denke, Sie könnten die generischen Parameter nicht mehr sinnvoll verwenden. Innerhalb der Klasse müssten sie wie Objekte behandelt werden, wenn sie von völlig unterschiedlichen Typen sein dürften. Das heißt, Sie können den generischen Parameter genauso gut weglassen und Überladungen bereitstellen.
Botz3000
3
@ Mansfield, weil eine orBeziehung Dinge viel zu allgemein macht, um nützlich zu sein. Sie würden haben Reflexion Figur zu tun, was zu tun und alle. (YUCK!).
Chris Pfohl
29

Die Antwort von Botz ist zu 100% richtig. Hier eine kurze Erklärung:

Wenn Sie eine Methode schreiben (generisch oder nicht) und die Typen der Parameter deklarieren, die die Methode verwendet, definieren Sie einen Vertrag:

Wenn Sie mir ein Objekt geben, das weiß, wie die Dinge zu tun sind, die Typ T zu tun weiß, kann ich entweder 'a' liefern: einen Rückgabewert des von mir deklarierten Typs oder 'b': eine Art Verhalten, das verwendet wird dieser Typ.

Wenn Sie versuchen, mehr als einen Typ gleichzeitig anzugeben (indem Sie ein oder haben) oder versuchen, einen Wert zurückzugeben, der mehr als einen Typ sein kann, wird der Vertrag unscharf:

Wenn Sie mir ein Objekt geben, das Seilspringen oder Pi auf die 15. Stelle berechnen kann, gebe ich entweder ein Objekt zurück, das angeln kann, oder mische vielleicht Beton.

Das Problem ist, dass Sie beim Einstieg in die Methode keine Ahnung haben, ob sie Ihnen eine IJumpRopeoder eine gegeben haben PiFactory. Wenn Sie die Methode verwenden (vorausgesetzt, Sie haben sie auf magische Weise kompiliert), sind Sie sich nicht sicher, ob Sie eine Fisheroder eine haben AbstractConcreteMixer. Grundsätzlich macht es das Ganze viel verwirrender.

Die Lösung für Ihr Problem ist eine von zwei Möglichkeiten:

  1. Definieren Sie mehr als eine Methode, die jede mögliche Transformation, jedes mögliche Verhalten oder was auch immer definiert. Das ist Botz 'Antwort. In der Programmierwelt wird dies als Überladen der Methode bezeichnet.

  2. Definieren Sie eine Basisklasse oder Schnittstelle, die alle für die Methode erforderlichen Aufgaben ausführen kann, und lassen Sie eine Methode genau diesen Typ verwenden. Dies kann das Einschließen eines stringund Exceptionin eine kleine Klasse beinhalten, um zu definieren, wie Sie sie der Implementierung zuordnen möchten, aber dann ist alles sehr klar und leicht zu lesen. Ich könnte in vier Jahren kommen und Ihren Code lesen und leicht verstehen, was los ist.

Welche Sie wählen, hängt davon ab, wie kompliziert Auswahl 1 und 2 wäre und wie erweiterbar sie sein muss.

Für Ihre spezielle Situation stelle ich mir vor, Sie ziehen nur eine Nachricht oder etwas aus der Ausnahme heraus:

public interface IHasMessage
{
    string GetMessage();
}

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

Jetzt brauchen Sie nur noch Methoden, die a stringund an Exceptionin eine IHasMessage umwandeln. Sehr leicht.

Chris Pfohl
quelle
sorry @ Botz3000, habe gerade bemerkt, dass ich deinen Namen falsch geschrieben habe.
Chris Pfohl
Oder der Compiler könnte den Typ als Vereinigungstyp innerhalb der Funktion bedrohen und den Rückgabewert vom gleichen Typ haben, in den er gelangt. TypeScript führt dies aus.
Alex
@Alex aber das macht C # nicht.
Chris Pfohl
Das ist wahr, aber es könnte. Ich habe diese Antwort so gelesen, dass sie nicht möglich wäre. Habe ich sie falsch interpretiert?
Alex
1
Die Sache ist, dass generische C # -Parametereinschränkungen und Generika selbst im Vergleich zu beispielsweise C ++ - Vorlagen ziemlich primitiv sind. In C # müssen Sie dem Compiler im Voraus mitteilen, welche Operationen für generische Typen zulässig sind. Die Möglichkeit, diese Informationen bereitzustellen, besteht darin, eine Implementierungsschnittstellenbeschränkung hinzuzufügen (wobei T: IDisposable). Möglicherweise möchten Sie jedoch nicht, dass Ihr Typ eine Schnittstelle implementiert, um eine generische Methode zu verwenden, oder Sie möchten möglicherweise einige Typen in Ihrem generischen Code zulassen, die keine gemeinsame Schnittstelle haben. Ex. Lassen Sie eine beliebige Struktur oder Zeichenfolge zu, damit Sie Equals (v1, v2) für einen wertbasierten Vergleich einfach aufrufen können.
Vakhtang
8

Wenn ChildClass bedeutet, dass es von ParentClass abgeleitet ist, können Sie einfach Folgendes schreiben, um sowohl ParentClass als auch ChildClass zu akzeptieren.

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

Wenn Sie andererseits zwei verschiedene Typen ohne Vererbungsbeziehung verwenden möchten, sollten Sie die Typen berücksichtigen, die dieselbe Schnittstelle implementieren.

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

dann können Sie Ihre generische Funktion schreiben als;

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}
Daryal
quelle
Gute Idee, außer ich glaube nicht, dass ich dazu in der Lage sein werde, da ich einen String verwenden möchte, der eine versiegelte Klasse gemäß dem obigen Kommentar ist ...
Mansfield,
Dies ist in der Tat ein Designproblem. Eine generische Funktion wird verwendet, um ähnliche Operationen mit der Wiederverwendung von Code auszuführen. beides, wenn Sie planen, verschiedene Operationen im Methodenkörper auszuführen, ist die Trennung von Methoden ein besserer Weg (IMHO).
Daryal
Tatsächlich schreibe ich eine einfache Fehlerprotokollierungsfunktion. Ich möchte, dass dieser letzte Parameter entweder eine Zeichenfolge mit Informationen zum Fehler oder eine Ausnahme ist. In diesem Fall speichere ich e.message + e.stacktrace trotzdem als Zeichenfolge.
Mansfield
Sie können eine neue Klasse mit isSuccesful schreiben, Nachricht und Ausnahme als Eigenschaften speichern. Dann können Sie prüfen, ob die Ausgabe wahr ist, und den Rest ausführen.
Daryal
1

So alt diese Frage auch ist, ich bekomme immer noch zufällige Upvotes zu meiner obigen Erklärung. Die Erklärung ist immer noch vollkommen in Ordnung, aber ich werde ein zweites Mal mit einem Typ antworten, der mir als Ersatz für Unionstypen dient (die stark typisierte Antwort auf die Frage, die von C # nicht direkt unterstützt wird) ).

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

Die obige Klasse stellt einen Typ dar, der entweder TP oder TA sein kann. Sie können es als solches verwenden (die Typen beziehen sich auf meine ursprüngliche Antwort):

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

Wichtige Notizen:

  • Sie erhalten Laufzeitfehler, wenn Sie dies nicht IsPrimaryzuerst überprüfen .
  • Sie können jedes von IsNeither IsPrimaryoder überprüfen IsAlternate.
  • Sie können über Primaryund auf den Wert zugreifenAlternate
  • Es gibt implizite Konverter zwischen TP / TA und Entweder, damit Sie entweder die Werte oder einen Eitherbeliebigen Ort übergeben können, an dem einer erwartet wird. Wenn Sie tun , ein PassEither , wo ein TAoder TPerwartet wird, aber der Eitherenthält die falsche Art von Wert finden Sie einen Laufzeitfehler erhalten.

Ich verwende dies normalerweise, wenn eine Methode entweder ein Ergebnis oder einen Fehler zurückgeben soll. Es bereinigt wirklich diesen Stilcode. Ich benutze dies auch sehr gelegentlich ( selten ) als Ersatz für Methodenüberladungen. Realistisch gesehen ist dies ein sehr schlechter Ersatz für eine solche Überlastung.

Chris Pfohl
quelle