Vincolo di tipo multiplo di metodo generico (OR)


135

Leggendo questo , ho imparato che era possibile consentire a un metodo di accettare parametri di più tipi rendendolo un metodo generico. Nell'esempio, il seguente codice viene utilizzato con un vincolo di tipo per garantire che "U" sia un IEnumerable<T>.

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

Ho trovato un po 'di codice in più che ha permesso di aggiungere più vincoli di tipo, come ad esempio:

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

Tuttavia, questo codice sembra imporre che argdeve essere sia un tipo di ParentClass e ChildClass . Quello che voglio fare è dire che arg potrebbe essere un tipo di ParentClass o ChildClass nel modo seguente:

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

Il tuo aiuto è apprezzato come sempre!


4
Cosa potresti fare utilmente, in modo generico all'interno del corpo di tale metodo (a meno che tutti i tipi multipli derivino tutti da una specifica classe base, nel qual caso perché non dichiararlo come vincolo del tipo)?
Damien_The_Unbeliever

@Damien_The_Unbeliever Non sei sicuro di cosa intendi nel corpo? A meno che tu non intenda consentire qualsiasi tipo e controllare manualmente nel corpo ... e nel codice effettivo che voglio scrivere (l'ultimo frammento di codice), vorrei essere in grado di passare una stringa o un'eccezione, quindi non esiste una classe relazione lì (tranne system.object immagino).
Mansfield

1
Si noti inoltre che non è utile per iscritto where T : string, come stringè una classe sigillata . L'unica cosa utile che potresti fare è definire i sovraccarichi per stringe T : Exception, come spiegato da @ Botz3000 nella sua risposta di seguito.
Mattias Buelens,

Ma quando non c'è relazione, gli unici metodi su cui puoi fare affidamento argsono quelli definiti da object- quindi perché non rimuovere semplicemente i generici dall'immagine e creare il tipo di arg object? Cosa stai guadagnando?
Damien_The_Unbeliever

1
@Mansfield È possibile creare un metodo privato che accetta un parametro oggetto. Entrambi i sovraccarichi lo chiamerebbero. Non sono necessari generici qui.
Botz3000,

Risposte:


68

Non è possibile. Tuttavia, è possibile definire sovraccarichi per tipi specifici:

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

Se fanno parte di una classe generica, saranno preferiti rispetto alla versione generica del metodo.


1
Interessante. Questo mi era venuto in mente, ma ho pensato che avrebbe potuto rendere il codice più pulito per usare una funzione. Ah bene, grazie mille! Solo per curiosità, sai se c'è un motivo specifico che non è possibile? (è stato lasciato deliberatamente fuori dalla lingua?)
Mansfield

3
@Mansfield Non conosco il motivo esatto, ma penso che non saresti più in grado di utilizzare i parametri generici in modo significativo. All'interno della classe, dovrebbero essere trattati come oggetti se gli fosse permesso di essere di tipi completamente diversi. Ciò significa che potresti anche tralasciare il parametro generico e fornire sovraccarichi.
Botz3000,

3
@Mansfield, è perché una orrelazione rende le cose lontane dal generale per essere utili. Si potrebbe avere a che fare riflessione per capire cosa fare e tutto il resto. (CHE SCHIFO!).
Chris Pfohl,

29

La risposta di Botz è corretta al 100%, ecco una breve spiegazione:

Quando si scrive un metodo (generico o meno) e si dichiarano i tipi di parametri adottati dal metodo, si definisce un contratto:

Se mi dai un oggetto che sa come fare l'insieme di cose che il Tipo T sa fare, posso fornire 'a': un valore di ritorno del tipo che dichiaro, o 'b': una sorta di comportamento che usa quel tipo.

Se provi a dargli più di un tipo alla volta (avendo un o) o provi a ottenerlo per restituire un valore che potrebbe essere più di un tipo, il contratto diventa sfocato:

Se mi dai un oggetto che sa saltare la corda o sa calcolare il pi alla 15a cifra, restituirò un oggetto che può andare a pescare o forse mescolare il cemento.

Il problema è che quando entri nel metodo non hai idea se ti hanno dato un IJumpRopeo un PiFactory. Inoltre, quando vai avanti e usi il metodo (supponendo che tu l'abbia compilato magicamente) non sei davvero sicuro di avere un Fishero un AbstractConcreteMixer. Fondamentalmente rende il tutto molto più confuso.

La soluzione al tuo problema è una delle due possibilità:

  1. Definire più di un metodo che definisce ogni possibile trasformazione, comportamento o altro. Questa è la risposta di Botz. Nel mondo della programmazione questo è indicato come sovraccarico del metodo.

  2. Definisci una classe di base o un'interfaccia che sappia come fare tutto ciò di cui hai bisogno per il metodo e che un metodo prenda proprio quel tipo. Ciò può comportare il completamento di una stringe Exceptionin una piccola classe per definire il modo in cui pianifichi di mapparli all'implementazione, ma poi tutto è super chiaro e facile da leggere. Potrei venire, tra quattro anni e leggere il tuo codice e capire facilmente cosa sta succedendo.

Quale scegli dipende da quanto complicate sarebbero le scelte 1 e 2 e quanto estensibile debba essere.

Quindi, per la tua situazione specifica, immaginerò che stai solo tirando fuori un messaggio o qualcosa dall'eccezione:

public interface IHasMessage
{
    string GetMessage();
}

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

Ora tutto ciò che serve sono i metodi che trasformano a stringe an Exceptionin IHasMessage. Molto facile.


scusa @ Botz3000, ho appena notato che ho scritto male il tuo nome.
Chris Pfohl,

Oppure il compilatore potrebbe minacciare il tipo come tipo di unione all'interno della funzione e avere il valore restituito dello stesso tipo in cui si trova. TypeScript lo fa.
Alex

@Alex ma non è quello che fa C #.
Chris Pfohl,

Questo è vero, ma potrebbe. Ho letto questa risposta perché non sarebbe stata in grado di interpretarla in modo errato?
Alex

1
Il fatto è che i vincoli di parametri generici C # e gli stessi generici sono piuttosto primitivi rispetto, per esempio, ai modelli C ++. C # richiede di comunicare in anticipo al compilatore quali operazioni sono consentite sui tipi generici. Il modo per fornire tali informazioni è aggiungere un vincolo di interfaccia implementa (dove T: IDisposable). Ma potresti non voler che il tuo tipo implementi qualche interfaccia per usare un metodo generico o potresti consentire alcuni tipi nel tuo codice generico che non hanno un'interfaccia comune. Ex. consentire qualsiasi struttura o stringa in modo da poter semplicemente chiamare Equals (v1, v2) per un confronto basato sul valore.
Vakhtang,

8

Se ChildClass significa che è derivato da ParentClass, puoi semplicemente scrivere quanto segue per accettare sia ParentClass che ChildClass;

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

D'altra parte, se si desidera utilizzare due tipi diversi senza alcuna relazione di ereditarietà tra loro, è necessario considerare i tipi che implementano la stessa interfaccia;

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

allora puoi scrivere la tua funzione generica come;

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

Buona idea, tranne per il fatto che non credo che sarò in grado di farlo poiché voglio usare una stringa che è una classe sigillata come da un commento sopra ...
Mansfield

questo è in effetti un problema di progettazione. una funzione generica viene utilizzata per eseguire operazioni simili con il riutilizzo del codice. entrambi se si prevede di eseguire diverse operazioni nel corpo del metodo, i metodi separati sono un modo migliore (IMHO).
daryal

Quello che sto facendo, infatti, è scrivere una semplice funzione di registrazione degli errori. Vorrei che l'ultimo parametro fosse una stringa di informazioni sull'errore o un'eccezione, nel qual caso salvo e.message + e.stacktrace come stringa.
Mansfield

puoi scrivere una nuova classe, avendo isSuccesful, salvare il messaggio e l'eccezione come proprietà. quindi puoi verificare se il problema è vero e fare il resto.
daryal

1

Vecchio come questa domanda ricevo ancora voti casuali sulla mia spiegazione sopra. La spiegazione è ancora perfettamente valida così com'è, ma risponderò una seconda volta con un tipo che mi è servito bene come sostituto per i tipi di unione (la risposta fortemente tipizzata alla domanda che non è direttamente supportata da C # così com'è ).

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 sopra rappresenta un tipo che può essere sia TP o TA. Puoi usarlo come tale (i tipi rimandano alla mia risposta originale):

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

Note importanti:

  • Riceverai errori di runtime se non controlli IsPrimaryprima.
  • Puoi controllare uno IsNeither IsPrimaryo IsAlternate.
  • È possibile accedere al valore tramite PrimaryeAlternate
  • Esistono convertitori impliciti tra TP / TA e O per consentire all'utente di passare i valori o Eitherovunque sia previsto. Se si fa passare un Eithercui una TAo TPci si aspetta, ma la Eithercontiene il tipo sbagliato di valore si otterrà un errore di runtime.

Di solito lo uso dove desidero che un metodo restituisca un risultato o un errore. Pulisce davvero quel codice di stile. Inoltre ogni tanto ( raramente ) lo uso come sostituto dei sovraccarichi del metodo. Realisticamente, questo è un pessimo sostituto di un tale sovraccarico.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.