nulo en genéricos de C #?

94

Tengo un método genérico que toma una solicitud y proporciona una respuesta.

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

Pero no siempre quiero una respuesta para mi solicitud, y no siempre quiero alimentar los datos de la solicitud para obtener una respuesta. Tampoco quiero tener que copiar y pegar métodos en su totalidad para realizar cambios menores. Lo que quiero es poder hacer esto:

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

¿Es esto factible de alguna manera? Parece que usar void específicamente no funciona, pero espero encontrar algo análogo.

dirección
fuente
1
¿Por qué no usar System.Object y hacer una verificación nula en DoSomething (respuesta Tres, solicitud Treq)?
James
Tenga en cuenta que debe utilizar el valor de retorno. Puede llamar a funciones como procedimientos. DoSomething(x);en lugar dey = DoSomething(x);
Olivier Jacot-Descombes
1
Creo que quiso decir: "Tenga en cuenta que no es necesario utilizar el valor de retorno". @ OlivierJacot-Descombes
zanedp

Respuestas:

95

No puede usar void, pero puede usar object: es un pequeño inconveniente porque sus posibles voidfunciones deben regresar null, pero si unifica su código, debería ser un pequeño precio a pagar.

Esta incapacidad para usar voidcomo tipo de retorno es al menos parcialmente responsable de una división entre las familias Func<...>y Action<...>de los delegados genéricos: si hubiera sido posible regresar void, todo Action<X,Y,Z>sería simple Func<X,Y,Z,void>. Desafortunadamente, esto no es posible.

dasblinkenlight
fuente
45
(Broma) Y todavía puede regresar voidde esos métodos que podrían ser nulos, con return System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(void));. Sin embargo, será un vacío en caja.
Jeppe Stig Nielsen
Como C # admite características de programación más funcionales, puede echar un vistazo a Unit que representa voiden FP. Y hay buenas razones para usarlo. En F #, todavía .NET, tenemos unitarchivos.
Joe
88

No, desafortunadamente no. Si voidfuera un tipo "real" (como uniten F # , por ejemplo) la vida sería mucho más simple en muchos sentidos. En particular, no necesitaríamos tanto a las familias Func<T>como a las Action<T>familias, simplemente habría en Func<void>lugar de Action, en Func<T, void>lugar de, Action<T>etc.

También simplificaría el async (no habría necesidad de un tipo no genérico Tasken absoluto), simplemente lo haríamos Task<void>.

Desafortunadamente, esa no es la forma en que funcionan los sistemas de tipo C # o .NET ...

Jon Skeet
fuente
4
Unfortunately, that's not the way the C# or .NET type systems work...Me estabas dando la esperanza de que tal vez las cosas pudieran funcionar así eventualmente. ¿Su último punto significa que no es probable que las cosas funcionen de esa manera?
Dave Cousineau
2
@Sahuagin: Sospecho que no, sería un cambio bastante grande en este punto.
Jon Skeet
1
@stannius: No, creo que es más un inconveniente que algo que conduce a un código incorrecto.
Jon Skeet
2
¿Existe la ventaja de tener un tipo de referencia de unidad sobre el tipo de valor vacío para representar "vacío"? El tipo de valor vacío me parece más adecuado, no tiene ningún valor y no ocupa espacio. Me pregunto por qué no se implementó void de esta manera. Sacarlo o no sacarlo de la pila no haría ninguna diferencia (hablando de código nativo, en IL podría ser diferente).
Ondrej Petrzilka
1
@Ondrej: He intentado usar una estructura vacía para cosas antes, y termina no estando vacía en el CLR ... Podría tener una carcasa especial, por supuesto. Más allá de eso, no sé cuál sugeriría; No lo he pensado mucho.
Jon Skeet
27

Esto es lo que puede hacer. Como dijo @JohnSkeet, no hay un tipo de unidad en C #, ¡así que hágalo usted mismo!

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

Ahora siempre puedes usar en Func<..., ThankYou>lugar deAction<...>

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

O use algo que ya haya hecho el equipo de Rx: http://msdn.microsoft.com/en-us/library/system.reactive.unit%28v=VS.103%29.aspx

Tridente D'Gao
fuente
System.Reactive.Unit es una buena sugerencia. A partir de noviembre de 2016, si está interesado en obtener una parte lo más pequeña posible del marco Reactive, porque no está usando nada más que la clase Unit, use el administrador de paquetes Install-Package System.Reactive.Core
nuget
6
Cambie el nombre de ThankYou a "KThx", y es un ganador. ^ _ ^Kthx.Bye;
LexH
Solo para comprobar que no me falta algo ... el captador Bye no agrega nada significativo sobre el acceso directo aquí, ¿verdad?
Andrew
1
@Andrew, no necesitas adiós, a menos que quieras seguir el espíritu de la codificación C #, que dice que no debes exponer campos
vacíos
16

Simplemente puede usarlo Objectcomo otros han sugerido. O Int32que he visto algún uso. El uso Int32introduce un número "ficticio" (uso 0), pero al menos no puede poner ningún objeto grande y exótico en una Int32referencia (las estructuras están selladas).

También puede escribir su propio tipo "vacío":

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

MyVoidSe permiten referencias (no es una clase estática) pero solo pueden ser null. El constructor de la instancia es privado (y si alguien intenta llamar a este constructor privado mediante la reflexión, se le lanzará una excepción).


Dado que se introdujeron las tuplas de valor (2017, .NET 4.7), tal vez sea natural usar la estructuraValueTuple (la tupla 0, la variante no genérica) en lugar de tal MyVoid. Su instancia tiene un ToString()que devuelve "()", por lo que parece una tupla cero. A partir de la versión actual de C #, no puede usar los tokens ()en el código para obtener una instancia. En su lugar, puede usar default(ValueTuple)o solo default(cuando el tipo se puede inferir del contexto).

Jeppe Stig Nielsen
fuente
2
Otro nombre para esto sería el patrón de objeto nulo (patrón de diseño).
Aelphaeis
@Aelphaeis Este concepto es un poco diferente de un patrón de objeto nulo. Aquí, el punto es tener algún tipo que podamos usar con un genérico. El objetivo con un patrón de objeto nulo es evitar escribir un método que devuelva nulo para indicar un caso especial y, en su lugar, devolver un objeto real con el comportamiento predeterminado adecuado.
Andrew Palmer
8

Me gusta la idea de Aleksey Bykov anterior, pero podría simplificarse un poco

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

Como no veo ninguna razón aparente por la cual Nothing.AtAll no podría simplemente dar null

La misma idea (o la de Jeppe Stig Nielsen) también es excelente para usar con clases escritas.

Por ejemplo, si el tipo solo se usa para describir los argumentos de un procedimiento / función que se pasa como argumento a algún método, y él mismo no toma ningún argumento.

(Todavía necesitará hacer una envoltura ficticia o permitir un "Nada" opcional. Pero en mi humilde opinión, el uso de la clase se ve bien con myClass <Nothing>)

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

o

void myProcWithNoArguments(Nothing Dummy=null){
    ...
}
Eske Rahn
fuente
1
El valor nulltiene la connotación de estar ausente o ausente object. Tener un solo valor para un Nothingsignifica que nunca se parece a nada más.
LexH
Es una buena idea, pero estoy de acuerdo con @LexieHankins. Creo que sería mejor que "nada en absoluto" fuera una instancia única de la clase Nothingalmacenada en un campo estático privado y agregando un constructor privado. El hecho de que null todavía sea posible es molesto, pero eso se resolverá, con suerte con C # 8.
Kirk Woll
Académicamente entiendo la distinción, pero sería un caso muy especial donde la distinción importa. (Imagine una función de tipo genérico que acepta valores NULL, donde el retorno de "nulo" se usa como un marcador especial, por ejemplo, como algún tipo de marcador de error)
Eske Rahn
4

void, aunque es un tipo, solo es válido como un tipo de retorno de un método.

No hay forma de evitar esta limitación de void.

Oded
fuente
1

Lo que hago actualmente es crear tipos sellados personalizados con constructor privado. Esto es mejor que lanzar excepciones en el c-tor porque no tiene que llegar hasta el tiempo de ejecución para darse cuenta de que la situación es incorrecta. Es sutilmente mejor que devolver una instancia estática porque no tiene que asignar ni una sola vez. Es sutilmente mejor que devolver un valor nulo estático porque es menos detallado en el lado de la llamada. Lo único que puede hacer la persona que llama es dar un valor nulo.

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

public sealed class None {
    private None() { }
}
Gru
fuente