Ograniczenie typu wielokrotnego metody ogólnej (OR)

135

Czytając to , dowiedziałem się, że można zezwolić metodzie na akceptowanie parametrów wielu typów, czyniąc ją metodą ogólną. W tym przykładzie poniższy kod jest używany z ograniczeniem typu, aby zapewnić, że „U” to IEnumerable<T>.

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

Znalazłem więcej kodu, który pozwolił na dodanie wielu ograniczeń typu, takich jak:

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

Jednak ten kod wydaje się wymuszać, że argmusi to być zarówno typ, jak ParentClass i ChildClass . Chcę tylko powiedzieć, że argument może być rodzajem ParentClass lub ChildClass w następujący sposób:

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

Twoja pomoc jest mile widziana jak zawsze!

Mansfield
źródło
4
Co możesz zrobić pożytecznie w sposób ogólny w treści takiej metody (chyba że wszystkie typy wielokrotne pochodzą z określonej klasy bazowej, w takim przypadku dlaczego nie zadeklarować tego jako ograniczenia typu)?
Damien_The_Unbeliever
@Damien_The_Unbeliever Nie wiesz, co masz na myśli mówiąc o ciele? Chyba że masz na myśli zezwolenie na dowolny typ i ręczne sprawdzenie w treści ... i w rzeczywistym kodzie, który chcę napisać (ostatni fragment kodu), chciałbym móc przekazać ciąg znaków LUB wyjątek, więc nie ma klasy tam związek (z wyjątkiem system.object, który sobie wyobrażam).
Mansfield
1
Pamiętaj również, że nie ma sensu na piśmie where T : string, ponieważ stringjest to klasa zapieczętowana . Jedyną przydatną rzeczą, jaką możesz zrobić, jest zdefiniowanie przeciążeń dla stringi T : Exception, jak wyjaśnił @ Botz3000 w jego odpowiedzi poniżej.
Mattias Buelens
Ale kiedy nie ma związku, jedynymi metodami, do których możesz się odwołać, argsą metody zdefiniowane przez object- dlaczego więc po prostu nie usunąć typów generycznych z obrazu i utworzyć typ arg object? Co zyskujesz?
Damien_The_Unbeliever
1
@Mansfield Możesz stworzyć prywatną metodę akceptującą parametr obiektu. Oba przeciążenia wywołałyby to. Nie potrzeba tutaj żadnych generyków.
Botz3000

Odpowiedzi:

68

Nie jest możliwe. Możesz jednak zdefiniować przeciążenia dla określonych typów:

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

Jeśli są one częścią klasy ogólnej, będą preferowane w stosunku do ogólnej wersji metody.

Botz3000
źródło
1
Ciekawy. Przyszło mi to do głowy, ale pomyślałem, że użycie jednej funkcji może sprawić, że kod będzie czystszy. Cóż, wielkie dzięki! Tak z ciekawości, czy wiesz, czy istnieje konkretny powód, dla którego nie jest to możliwe? (czy celowo pominięto to w języku?)
Mansfield
3
@Mansfield Nie znam dokładnego powodu, ale myślę, że nie byłbyś już w stanie używać parametrów ogólnych w znaczący sposób. W klasie musieliby być traktowani jak przedmioty, gdyby pozwolono im być zupełnie innego typu. Oznacza to, że równie dobrze można pominąć parametr ogólny i zapewnić przeciążenia.
Botz3000
3
@Mansfield, to dlatego, że orzwiązek sprawia, że ​​rzeczy są zbyt ogólne, aby były przydatne. Chcesz mieć zrobić odbicie, aby dowiedzieć się, co zrobić, i to wszystko. (Fuj!).
Chris Pfohl
29

Odpowiedź Botza jest w 100% poprawna, oto krótkie wyjaśnienie:

Kiedy piszesz metodę (ogólną lub nie) i deklarujesz typy parametrów, które metoda przyjmuje, definiujesz kontrakt:

Jeśli dasz mi obiekt, który wie, jak wykonać zestaw rzeczy, które Typ T wie, jak to zrobić, mogę dostarczyć albo „a”: wartość zwracaną typu, który zadeklarowałem, lub „b”: jakieś zachowanie, które wykorzystuje tego typu.

Jeśli spróbujesz podać więcej niż jeden typ na raz (przez posiadanie lub) lub spróbujesz sprawić, aby zwracał wartość, która może być więcej niż jednego typu, kontrakt stanie się rozmyty:

Jeśli dasz mi obiekt, który wie, jak skakać na linie lub umie obliczyć liczbę pi do 15 cyfry, zwrócę obiekt, który można łowić na ryby, albo może mieszać beton.

Problem polega na tym, że kiedy wchodzisz do metody, nie masz pojęcia, czy dali ci IJumpRopeczy PiFactory. Co więcej, kiedy korzystasz z metody (zakładając, że udało ci się ją magicznie skompilować), nie jesteś pewien, czy masz Fisherplik AbstractConcreteMixer. Zasadniczo sprawia, że ​​całość jest o wiele bardziej zagmatwana.

Rozwiązanie twojego problemu jest jedną z dwóch możliwości:

  1. Zdefiniuj więcej niż jedną metodę, która definiuje każdą możliwą transformację, zachowanie lub cokolwiek innego. To odpowiedź Botza. W świecie programowania nazywa się to przeciążaniem metody.

  2. Zdefiniuj klasę bazową lub interfejs, który wie, jak zrobić wszystko, czego potrzebujesz dla metody i niech jedna metoda przyjmuje tylko ten typ. Może to obejmować zamknięcie stringi Exceptionw małej klasie, aby zdefiniować, w jaki sposób planujesz odwzorować je na implementację, ale wtedy wszystko jest bardzo jasne i łatwe do odczytania. Mógłbym przyjść za cztery lata i przeczytać twój kod i łatwo zrozumieć, co się dzieje.

To, który wybierzesz, zależy od tego, jak skomplikowany byłby wybór 1 i 2 i jak musi być rozszerzalny.

Więc w twojej konkretnej sytuacji wyobrażam sobie, że po prostu wyciągasz wiadomość lub coś z wyjątku:

public interface IHasMessage
{
    string GetMessage();
}

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

Teraz wszystko, czego potrzebujesz, to metody, które przekształcają a stringi an Exceptionw IHasMessage. Bardzo łatwe.

Chris Pfohl
źródło
przepraszam @ Botz3000, właśnie zauważyłem, że błędnie wpisałem twoje imię.
Chris Pfohl,
Lub kompilator może zagrozić typowi jako typowi unii w funkcji i mieć wartość zwracaną tego samego typu, jaki otrzyma. TypeScript to robi.
Alex
@Alex, ale nie to robi C #.
Chris Pfohl
To prawda, ale może. Przeczytałem tę odpowiedź, bo nie da rady, czy źle zinterpretowałem?
Alex
1
Chodzi o to, że ogólne ograniczenia parametrów języka C # i same typy ogólne są dość prymitywne w porównaniu z, powiedzmy, szablonami C ++. C # wymaga wcześniejszego poinformowania kompilatora, jakie operacje są dozwolone na typach ogólnych. Sposobem na podanie tych informacji jest dodanie ograniczenia interfejsu implementacji (gdzie T: IDisposable). Ale możesz nie chcieć, aby Twój typ implementował jakiś interfejs do używania metody ogólnej lub możesz chcieć zezwolić na niektóre typy w swoim kodzie ogólnym, które nie mają wspólnego interfejsu. Dawny. zezwalaj na dowolną strukturę lub ciąg, aby można było po prostu wywołać Equals (v1, v2) w celu porównania opartego na wartości.
Vakhtang
8

Jeśli ChildClass oznacza, że ​​pochodzi z ParentClass, możesz po prostu napisać następujący tekst, aby zaakceptować zarówno ParentClass, jak i ChildClass;

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

Z drugiej strony, jeśli chcesz użyć dwóch różnych typów bez relacji dziedziczenia między nimi, powinieneś rozważyć typy implementujące ten sam interfejs;

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

wtedy możesz napisać swoją ogólną funkcję jako;

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}
daryal
źródło
Dobry pomysł, ale nie sądzę, żebym był w stanie to zrobić, ponieważ chcę użyć łańcucha, który jest zapieczętowaną klasą zgodnie z komentarzem powyżej ...
Mansfield,
w rzeczywistości jest to problem projektowy. funkcja ogólna jest używana do wykonywania podobnych operacji z ponownym wykorzystaniem kodu. jeśli planujesz wykonywać różne operacje w treści metody, oddzielenie metod jest lepszym sposobem (IMHO).
daryal
W rzeczywistości piszę prostą funkcję rejestrowania błędów. Chciałbym, aby ostatni parametr był albo ciągiem informacji o błędzie, albo wyjątkiem, w którym to przypadku i tak zapisuję e.message + e.stacktrace jako ciąg.
Mansfield
możesz napisać nową klasę, mając isSuccesful, zapisać wiadomość i wyjątek jako właściwości. wtedy możesz sprawdzić, czy jest to prawdą i zrobić pozostałe.
daryal
1

Tak stare, jak to pytanie, wciąż otrzymuję losowe głosy za moim wyjaśnieniem powyżej. Wyjaśnienie nadal jest w porządku i tak jest, ale odpowiem po raz drugi typem, który służył mi dobrze jako substytut typów unii (silnie wpisana odpowiedź na pytanie, które nie jest bezpośrednio obsługiwane przez C #, jak jest ).

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

Powyższe klasy reprezentuje typ, który może być albo TP lub Ta. Możesz go użyć jako takiego (typy odnoszą się do mojej oryginalnej odpowiedzi):

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

Ważne notatki:

  • Jeśli nie sprawdzisz IsPrimarynajpierw, otrzymasz błędy czasu wykonania .
  • Możesz sprawdzić dowolne z IsNeither IsPrimarylub IsAlternate.
  • Możesz uzyskać dostęp do wartości za pomocą PrimaryiAlternate
  • Istnieją niejawne konwertery między TP / TA a Albo, aby umożliwić przekazywanie wartości lub Eitherwszędzie tam, gdzie jest to oczekiwane. Jeśli zrobić zdać Eithergdzie TAalbo TPma, ale Eitherzawiera niewłaściwy typ wartości dostaniesz błąd wykonania.

Zwykle używam tego, gdy chcę, aby metoda zwracała wynik lub błąd. Naprawdę oczyszcza ten kod stylu. Bardzo rzadko ( rzadko ) używam tego jako zamiennika przeciążeń metod. Realistycznie jest to bardzo kiepski substytut takiego przeciążenia.

Chris Pfohl
źródło