nieważne w C # rodzajach ogólnych?

94

Mam metodę ogólną, która przyjmuje żądanie i zapewnia odpowiedź.

public Tres DoSomething<Tres, Treq>(Tres response, Treq request)
{/*stuff*/}

Ale nie zawsze chcę odpowiedzi na moje żądanie i nie zawsze chcę podawać dane żądania, aby uzyskać odpowiedź. Nie chcę też kopiować i wklejać metod w całości, aby wprowadzić drobne zmiany. To, czego chcę, to móc to zrobić:

public Tre DoSomething<Tres>(Tres response)
{
    return DoSomething<Tres, void>(response, null);
}

Czy jest to w jakiś sposób wykonalne? Wygląda na to, że użycie void nie działa, ale mam nadzieję, że znajdę coś analogicznego.

reżyseria
źródło
1
Dlaczego po prostu nie użyć System.Object i nie sprawdzić wartości null w DoSomething (odpowiedź Tres, żądanie Treq)?
James,
Zauważ, że musisz użyć wartości zwracanej. Możesz wywoływać funkcje, takie jak procedury. DoSomething(x);zamiasty = DoSomething(x);
Olivier Jacot-Descombes
1
Myślę, że chciałeś powiedzieć: „Zauważ, że nie musisz używać wartości zwracanej”. @ OlivierJacot-Descombes
zanedp

Odpowiedzi:

97

Nie możesz użyć void, ale możesz użyć object: jest to trochę niewygodne, ponieważ Twoje przyszłe voidfunkcje muszą zostać zwrócone null, ale jeśli ujednolici Twój kod, powinna to być niewielka cena.

Ta niezdolność do wykorzystania voidjako typ zwracany jest przynajmniej częściowo odpowiedzialny za rozłam pomiędzy Func<...>i Action<...>rodzin delegatów generycznych: gdyby było to możliwe, aby wrócić void, wszystko Action<X,Y,Z>stanie się po prostu Func<X,Y,Z,void>. Niestety nie jest to możliwe.

Sergey Kalinichenko
źródło
47
(Żart) I nadal może powrócić voidz tych niedoszłych metod return System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(void));. Będzie to jednak pustka w pudełku.
Jeppe Stig Nielsen
1
Ponieważ C # obsługuje bardziej funkcjonalne funkcje programowania, możesz przyjrzeć się Unit, który reprezentuje voidw FP. I są dobre powody, by go używać. W języku F #, nadal .NET, mamy unitwbudowany.
joe
88

Nie, niestety nie. Gdyby voidbył to typ „prawdziwy” (jak unitna przykład F # ), życie byłoby o wiele prostsze na wiele sposobów. W szczególności nie potrzebowalibyśmy zarówno rodziny, jak Func<T>i Action<T>- po prostu byłoby Func<void>zamiast Action, Func<T, void>zamiast Action<T>itp.

Ułatwiłoby to również asynchronizację - w ogóle nie byłoby potrzeby stosowania Tasktypu nieogólnego - po prostu byśmy to zrobili Task<void>.

Niestety tak nie działa system typu C # czy .NET ...

Jon Skeet
źródło
4
Unfortunately, that's not the way the C# or .NET type systems work...Sprawiałeś, że miałem nadzieję, że może w końcu tak się wszystko potoczy. Czy twoja ostatnia uwaga oznacza, że ​​prawdopodobnie nigdy nie będziemy mieć takiego rozwiązania?
Dave Cousineau
2
@Sahuagin: Podejrzewam, że nie - w tym momencie byłaby to całkiem duża zmiana.
Jon Skeet,
1
@stannius: Nie, myślę, że jest to bardziej kłopotliwe niż coś, co prowadzi do nieprawidłowego kodu.
Jon Skeet,
2
Czy istnieje przewaga typu odwołania Unit nad pustym typem wartości reprezentującym „void”? Pusty typ wartości wydaje mi się bardziej odpowiedni, nie ma wartości i nie zajmuje miejsca. Zastanawiam się, dlaczego pustka nie została zaimplementowana w ten sposób. Wyskakiwanie lub nie zdejmowanie go ze stosu nie miałoby znaczenia (mówienie natywnego kodu, w IL może być inaczej).
Ondrej Petrzilka
1
@Ondrej: Próbowałem już wcześniej użyć pustej struktury dla rzeczy, ale kończy się to, że nie jest ona pusta w CLR ... Oczywiście może to być specjalna wielkość. Poza tym nie wiem, co bym sugerował; Nie myślałem o tym dużo.
Jon Skeet
27

Oto, co możesz zrobić. Jak powiedział @JohnSkeet, w C # nie ma typu jednostki, więc zrób to sam!

public sealed class ThankYou {
   private ThankYou() { }
   private readonly static ThankYou bye = new ThankYou();
   public static ThankYou Bye { get { return bye; } }
}

Teraz możesz zawsze użyć Func<..., ThankYou>zamiastAction<...>

public ThankYou MethodWithNoResult() {
   /* do things */
   return ThankYou.Bye;
}

Lub użyj czegoś już stworzonego przez zespół Rx: http://msdn.microsoft.com/en-us/library/system.reactive.unit%28v=VS.103%29.aspx

Trident D'Gao
źródło
System.Reactive.Unit to dobra sugestia. Od listopada 2016 r., Jeśli chcesz uzyskać jak najmniejszy fragment platformy Reactive, ponieważ nie używasz niczego innego niż klasa Unit, użyj menedżera pakietów Install-Package System.Reactive.Core
nuget
6
Zmień nazwę ThankYou na „KThx” i to jest zwycięzca. ^ _ ^Kthx.Bye;
LexH,
Żeby sprawdzić, czy czegoś mi nie brakuje. Funkcja Bye getter nie dodaje nic znaczącego w porównaniu z bezpośrednim dostępem, prawda?
Andrew,
1
@Andrew, nie potrzebujesz bye getter, chyba że chcesz podążać za duchem kodowania C #, który mówi, że nie powinieneś eksponować nagich pól
Trident D'Gao
16

Możesz po prostu użyć Objecttego, co sugerowali inni. Lub Int32które widziałem w pewnym sensie. Użycie Int32wprowadza "fikcyjną" liczbę (użyj 0), ale przynajmniej nie możesz umieścić żadnego dużego i egzotycznego obiektu w Int32referencji (struktury są zapieczętowane).

Możesz też napisać własny typ „void”:

public sealed class MyVoid
{
  MyVoid()
  {
    throw new InvalidOperationException("Don't instantiate MyVoid.");
  }
}

MyVoidodniesienia są dozwolone (nie jest to klasa statyczna), ale mogą być tylko null. Konstruktor instancji jest prywatny (i jeśli ktoś spróbuje wywołać ten prywatny konstruktor przez odbicie, zostanie do niego zgłoszony wyjątek).


Ponieważ wprowadzono krotki wartości (2017, .NET 4,7), może być naturalne użycie strukturyValueTuple (krotka 0, wariant nieogólny) zamiast takiego MyVoid. Jego wystąpienie ma ToString()zwracaną wartość "()", więc wygląda jak zerowa krotka. W obecnej wersji C # nie można użyć tokenów ()w kodzie, aby uzyskać wystąpienie. Możesz zamiast tego użyć default(ValueTuple)lub po prostu default(gdy typ można wywnioskować z kontekstu).

Jeppe Stig Nielsen
źródło
2
Inną nazwą tego byłby wzorzec obiektu zerowego (wzorzec projektowy).
Aelphaeis
@Aelphaeis Ta koncepcja różni się nieco od wzorca obiektu zerowego. Tutaj chodzi o to, aby mieć jakiś typ, którego możemy użyć z generycznym. Celem wzorca obiektu o wartości null jest uniknięcie pisania metody, która zwraca wartość null w celu wskazania specjalnego przypadku i zamiast tego zwraca rzeczywisty obiekt z odpowiednim zachowaniem domyślnym.
Andrew Palmer
8

Podoba mi się powyższy pomysł Aleksieja Bykowa, ale można go trochę uprościć

public sealed class Nothing {
    public static Nothing AtAll { get { return null; } }
}

Ponieważ nie widzę wyraźnego powodu, dla którego Nothing.AtAll nie może po prostu dać null

Ten sam pomysł (lub ten autorstwa Jeppe Stiga Nielsena) jest również świetny do użycia z klasami typizowanymi.

Np. Jeśli typ jest używany tylko do opisu argumentów procedury / funkcji przekazanej jako argument do jakiejś metody, a sama nie przyjmuje żadnych argumentów.

(Nadal będziesz musiał albo utworzyć ślepą otokę, albo zezwolić na opcjonalne „Nic”. Ale IMHO użycie klasy wygląda ładnie z myClass <Nothing>)

void myProcWithNoArguments(Nothing Dummy){
     myProcWithNoArguments(){
}

lub

void myProcWithNoArguments(Nothing Dummy=null){
    ...
}
Eske Rahn
źródło
1
Wartość nullma konotację braku lub nieobecności object. Posiadanie tylko jednej wartości a Nothingoznacza, że ​​nigdy nie wygląda jak nic innego.
LexH,
To dobry pomysł, ale zgadzam się z @LexieHankins. Myślę, że lepiej byłoby, gdyby „w ogóle nic” było unikalną instancją klasy Nothingprzechowywaną w prywatnym polu statycznym i dodającą prywatnego konstruktora. Fakt, że wartość null jest nadal możliwa, jest denerwujący, ale zostanie to rozwiązane, miejmy nadzieję, w C # 8.
Kirk Woll,
Z naukowego punktu widzenia rozumiem to rozróżnienie, ale byłby to bardzo szczególny przypadek, w którym to rozróżnienie ma znaczenie. (Wyobraź sobie funkcję ogólnego typu dopuszczającego wartość null, w której zwrot „null” jest używany jako specjalny znacznik, np. Jako pewnego rodzaju znacznik błędu)
Eske Rahn
4

void, choć typ, jest poprawny tylko jako typ zwracany metody.

Nie ma sposobu na obejście tego ograniczenia void.

Oded
źródło
1

Obecnie tworzę niestandardowe zapieczętowane typy za pomocą prywatnego konstruktora. Jest to lepsze niż rzucanie wyjątków w c-tor, ponieważ nie musisz uzyskiwać czasu do uruchomienia, aby dowiedzieć się, że sytuacja jest nieprawidłowa. Jest to subtelnie lepsze niż zwracanie instancji statycznej, ponieważ nie trzeba przydzielać ani razu. Jest to subtelnie lepsze niż zwracanie statycznej wartości null, ponieważ jest mniej rozwlekłe po stronie wywołania. Jedyne, co dzwoniący może zrobić, to podać wartość null.

public sealed class Void {
    private Void() { }
}

public sealed class None {
    private None() { }
}
Gru
źródło