vazio em genéricos C #?


94

Tenho um método genérico que recebe uma solicitação e fornece uma resposta.

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

Mas nem sempre quero uma resposta para minha solicitação e nem sempre quero alimentar os dados da solicitação para obter uma resposta. Também não quero ter que copiar e colar métodos inteiramente para fazer pequenas alterações. O que eu quero é ser capaz de fazer isso:

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

Isso é viável de alguma maneira? Parece que usar especificamente o vazio não funciona, mas espero encontrar algo análogo.


1
Por que não apenas usar System.Object e fazer uma verificação nula em DoSomething (resposta Tres, solicitação Treq)?
James

Observe que você precisa usar o valor de retorno. Você pode chamar funções como procedimentos. DoSomething(x);em vez dey = DoSomething(x);
Olivier Jacot-Descombes

1
Acho que você quis dizer: "Observe que você não precisa usar o valor de retorno." @ OlivierJacot-Descombes
zanedp

Respostas:


95

Você não pode usar void, mas pode usar object: é um pouco inconveniente porque suas supostas voidfunções precisam retornar null, mas se isso unificar seu código, deverá ser um pequeno preço a pagar.

Essa incapacidade de usar voidcomo um tipo de retorno é pelo menos parcialmente responsável por uma divisão entre as famílias Func<...>e Action<...>dos delegados genéricos: se fosse possível retornar void, todos Action<X,Y,Z>se tornariam simples Func<X,Y,Z,void>. Infelizmente, isto não é possível.


45
(Piada) E ele ainda pode retornar voiddaqueles métodos que seriam vazios, com return System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(void));. No entanto, será um vazio encaixotado.
Jeppe Stig Nielsen

Como C # oferece suporte a recursos de programação mais funcionais, você pode dar uma olhada em Unit que representa voidno FP. E há boas razões para usá-lo. Em F #, ainda .NET, temos um unitbuilt-in.
joe

88

Não, infelizmente não. Se voidfosse um tipo "real" (como unitem F # , por exemplo), a vida seria muito mais simples de várias maneiras. Em particular, não precisaríamos das famílias Func<T>e Action<T>- haveria apenas em Func<void>vez de Action, em Func<T, void>vez de Action<T>etc.

Isso também tornaria o async mais simples - não haveria necessidade do Tasktipo não genérico - nós apenas teríamos Task<void>.

Infelizmente, não é assim que funcionam os sistemas do tipo C # ou .NET ...


4
Unfortunately, that's not the way the C# or .NET type systems work...Você estava me deixando esperançoso de que talvez as coisas funcionassem assim eventualmente. Seu último ponto significa que provavelmente nunca faremos as coisas funcionarem dessa maneira?
Dave Cousineau

2
@Sahuagin: Suspeito que não - seria uma grande mudança neste momento.
Jon Skeet

1
@stannius: Não, é mais um inconveniente do que algo que leva a um código incorreto, eu acho.
Jon Skeet

2
Há vantagem em ter o tipo de referência de unidade sobre o tipo de valor vazio para representar "vazio"? O tipo de valor vazio parece mais adequado para mim, não carrega nenhum valor e não ocupa espaço. Eu me pergunto por que o void não foi implementado dessa forma. Popping ou não da pilha não faria diferença (falando código nativo, em IL pode ser diferente).
Ondrej Petrzilka

1
@Ondrej: Já tentei usar uma estrutura vazia para as coisas antes, e ela acaba não ficando vazia no CLR ... Poderia ser um caso especial, é claro. Além disso, não sei o que sugerir; Eu não pensei muito sobre isso.
Jon Skeet

27

Aqui está o que você pode fazer. Como @JohnSkeet disse, não há tipo de unidade em C #, então faça você mesmo!

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

Agora você sempre pode usar em Func<..., ThankYou>vez deAction<...>

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

Ou use algo já feito pela equipe Rx: http://msdn.microsoft.com/en-us/library/system.reactive.unit%28v=VS.103%29.aspx


System.Reactive.Unit é uma boa sugestão. Em novembro de 2016, se você estiver interessado em obter o menor pedaço possível do framework Reactive, porque você não está usando nada além da classe Unit, use o gerenciador de pacotes Install-Package System.Reactive.Core
nuget

6
Renomeie ThankYou para "KThx" e é um vencedor. ^ _ ^Kthx.Bye;
LexH

Só para verificar se não estou perdendo nada ... o getter Bye não adiciona nada significativo sobre o acesso direto aqui, adiciona?
Andrew,

1
@Andrew, você não precisa de tchau getter, a menos que queira seguir o espírito da codificação C #, que diz que você não deve expor campos
vazios

16

Você pode simplesmente usar Objectcomo outros sugeriram. Ou Int32que vi alguma utilidade. Usar Int32introduz um número "fictício" (usar 0), mas pelo menos você não pode colocar nenhum objeto grande e exótico em uma Int32referência (as estruturas são lacradas).

Você também pode escrever seu próprio tipo "vazio":

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

MyVoidreferências são permitidas (não é uma classe estática), mas só podem ser null. O construtor da instância é privado (e se alguém tentar chamar este construtor privado por meio de reflexão, uma exceção será lançada para eles).


Como as tuplas de valor foram introduzidas (2017, .NET 4.7), talvez seja natural usar a estruturaValueTuple (a tupla 0, a variante não genérica) em vez de uma MyVoid. Sua instância tem um ToString()que retorna "()", por isso parece uma tupla zero. A partir da versão atual do C #, você não pode usar os tokens ()no código para obter uma instância. Você pode usar default(ValueTuple)ou apenas default(quando o tipo pode ser inferido a partir do contexto).


2
Outro nome para isso seria o padrão de objeto nulo (padrão de design).
Aelphaeis

@Aelphaeis Este conceito é um pouco diferente de um padrão de objeto nulo. Aqui, a questão é apenas ter algum tipo que possamos usar com um genérico. O objetivo com um padrão de objeto nulo é evitar a escrita de um método que retorna nulo para indicar um caso especial e, em vez disso, retorna um objeto real com comportamento padrão apropriado.
Andrew Palmer

8

Gosto da ideia de Aleksey Bykov acima, mas poderia ser um pouco simplificada

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

Como não vejo nenhuma razão aparente para que Nothing.AtAll não pudesse simplesmente dar null

A mesma ideia (ou a de Jeppe Stig Nielsen) também é ótima para uso com classes digitadas.

Por exemplo, se o tipo é usado apenas para descrever os argumentos para um procedimento / função passado como um argumento para algum método, e ele próprio não aceita nenhum argumento.

(Você ainda precisará fazer um invólucro fictício ou permitir um "Nada" opcional. Mas, IMHO, o uso da classe parece bom com myClass <Nada>)

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

ou

void myProcWithNoArguments(Nothing Dummy=null){
    ...
}

1
O valor nulltem a conotação de estar ausente ou ausente object. Ter apenas um valor para Nothingsignifica que ele nunca se parece com qualquer outra coisa.
LexH

É uma boa ideia, mas concordo com @LexieHankins. Eu acho que seria melhor "nada" ser uma instância única da classe Nothingarmazenada em um campo estático privado e adicionando um construtor privado. O fato de null ainda ser possível é irritante, mas será resolvido, esperançosamente com C # 8.
Kirk Woll

Academicamente eu entendo a distinção, mas seria um caso muito especial em que a distinção fosse importante. (Imagine uma função do tipo anulável genérico, onde o retorno de "nulo" é usado como um marcador especial, por exemplo, como algum tipo de marcador de erro)
Eske Rahn

4

void, embora seja um tipo, só é válido como um tipo de retorno de um método.

Não há como contornar essa limitação de void.


1

O que eu faço atualmente é criar tipos selados personalizados com construtor privado. Isso é melhor do que lançar exceções no c-tor porque você não precisa chegar até o tempo de execução para descobrir que a situação está incorreta. É sutilmente melhor do que retornar uma instância estática, porque você não precisa alocar nem uma vez. É sutilmente melhor do que retornar um null estático porque é menos detalhado no lado da chamada. A única coisa que o chamador pode fazer é fornecer null.

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

public sealed class None {
    private None() { }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.