As funções devem retornar um objeto nulo ou vazio?


209

Qual é a melhor prática ao retornar dados de funções. É melhor retornar um objeto nulo ou vazio? E por que um deveria fazer um sobre o outro?

Considere isto:

public UserEntity GetUserById(Guid userId)
{
     //Imagine some code here to access database.....

     //Check if data was returned and return a null if none found
     if (!DataExists)
        return null; 
        //Should I be doing this here instead? 
        //return new UserEntity();  
     else
        return existingUserEntity;
}

Vamos fingir que haveria casos válidos neste programa que não haveriam informações do usuário no banco de dados com esse GUID. Eu imagino que não seria apropriado lançar uma exceção neste caso? Também tenho a impressão de que o tratamento de exceções pode prejudicar o desempenho.


4
Eu acho que você quer dizer if (!DataExists).
23920 Sarah Vessels

107
Esta é uma questão arquitetônica e é perfeitamente apropriada. A pergunta do OP é válida independentemente do problema de negócios que está tentando resolver.
31410 Joseph Ferris

2
Esta pergunta já foi suficientemente respondida. Eu acho que essa é uma pergunta muito interessante.
John Scipione

'getUser ()' deve retornar nulo. 'getCurrentUserInfo ()' ou 'getCurrentPermissions ()', OTOH, seriam perguntas mais reveladoras - eles devem retornar um objeto de resposta não nulo, independentemente de quem / ou se alguém está logado.
Thomas W

2
Nenhum @Bergi o outro é duplicado. A mina foi solicitada primeiro, em outubro, a outra foi solicitada três mariposas no final de dezembro. Além disso, o outro fala sobre uma coleção que é um pouco diferente.
7wp

Respostas:


207

Retornar nulo geralmente é a melhor ideia se você pretende indicar que nenhum dado está disponível.

Um objeto vazio indica que os dados foram retornados, enquanto o retorno nulo indica claramente que nada foi retornado.

Além disso, retornar um nulo resultará em uma exceção nula se você tentar acessar membros no objeto, o que pode ser útil para destacar o código de buggy - tentar acessar um membro do nada não faz sentido. O acesso a membros de um objeto vazio não falhará, o que significa que os erros podem não ser descobertos.


21
Você deve lançar uma exceção, não engolir o problema e retornar nulo. No mínimo, você deve registrar isso e continuar.
31710 Chris Ballance

130
@ Chris: Eu discordo. Se o código documentar claramente que o valor de retorno é nulo, é perfeitamente aceitável retornar nulo se nenhum resultado for encontrado para corresponder aos seus critérios. Lançar uma exceção deve ser sua ÚLTIMA escolha, não sua PRIMEIRA.
Mike Hofer

12
@ Chris: Com que base você decide isso? Adicionar registro à equação certamente parece excessivo. Deixe o código que está consumindo decidir o que - se houver - deve ser feito no caso de nenhum usuário. Como no meu comentário anterior, não há absolutamente nenhum problema em retornar um valor universalmente definido como "sem dados".
26610 Adam Robinson

17
Estou um pouco confuso que um desenvolvedor da Microsoft acredite que "retornar nulo" equivale a "engolir o problema". Se a memória servir, existem vários métodos no Framework em que os métodos null são retornados se não houver nada que corresponda à solicitação do chamador. Isso é "engolir o problema?"
Mike Hofer

5
Por último, mas não menos importante, haveria bool GetUserById(Guid userId, out UserEntity result)- que eu preferiria com o valor de retorno "nulo" e que não é tão extremo quanto lançar uma exceção. Ele permite um nullcódigo livre bonito como if(GetUserById(x,u)) { ... }.
Marcel Jackwerth

44

Depende do que faz mais sentido para o seu caso.

Faz sentido retornar nulo, por exemplo, "esse usuário não existe"?

Ou faz sentido criar um usuário padrão? Isso faz mais sentido quando você pode assumir com segurança que, se um usuário NÃO EXISTE, o código de chamada pretende que exista um quando ele solicitar.

Ou faz sentido lançar uma exceção (à la "FileNotFound") se o código de chamada estiver exigindo um usuário com um ID inválido?

No entanto - do ponto de vista da separação de preocupações / SRP, os dois primeiros estão mais corretos. E tecnicamente o primeiro é o mais correto (mas apenas por um fio de cabelo) - GetUserById deve ser o único responsável por uma coisa: obter o usuário. Manipular seu próprio caso "o usuário não existe" retornando outra coisa pode ser uma violação do SRP. Separar em uma verificação diferente - bool DoesUserExist(id)seria apropriado se você optar por lançar uma exceção.

Com base nos extensos comentários abaixo : se essa é uma pergunta de design no nível da API, esse método pode ser análogo ao "OpenFile" ou "ReadEntireFile". Estamos "abrindo" um usuário de algum repositório e hidratando o objeto a partir dos dados resultantes. Uma exceção pode ser apropriada neste caso. Pode não ser, mas poderia ser.

Todas as abordagens são aceitáveis ​​- depende apenas, com base no contexto maior da API / aplicativo.


Alguém votou em você e eu votei em você de novo, pois isso não me parece uma má resposta; exceto: eu nunca lançaria uma exceção ao encontrar nenhum usuário em um método como o que o pôster fornece. Se encontrar nenhum usuário implica um ID inválido ou algum problema desse tipo de exceção digna, que deve acontecer mais acima - o método de arremesso necessidades para saber mais sobre onde esse ID veio, etc.
Jacob Mattison

(Eu estou supondo que o downvote foi uma objeção à idéia de lançar uma exceção em uma circunstância como esta.)
Jacob Mattison

1
Concordo até o seu último ponto. Não há violação do SRP retornando um valor universalmente definido como "sem dados". É como afirmar que um banco de dados SQL deve retornar um erro se uma cláusula where não produzir resultados. Embora uma exceção seja uma opção de design válida (embora possa me irritar como consumidor), ela não é "mais correta" do que retornar nulo. E não, eu não sou o DV.
26610 Adam Robinson

@ JacobM lançamos exceções quando exigimos um caminho do sistema de arquivos que não existe, não retorna nulo, mas não dos bancos de dados. Então, claramente, ambos são apropriados, e é nisso que estou entrando - só depende.
Rex M

2
@ Charles: Você está respondendo à pergunta "uma exceção deve ser lançada em algum momento", mas a pergunta é "essa função deve lançar a exceção". A resposta correta é "talvez", não "sim".
26610 Adam Robinson

30

Pessoalmente, eu uso NULL. Deixa claro que não há dados a serem retornados. Mas há casos em que um objeto nulo pode ser útil.


Estou prestes a adicionar isso como uma resposta. NullObjectPattern ou padrão de caso especial. Então você poderia implementar um para cada caso, NoUserEntitiesFound, NullUserEntities etc.
David Swindells

27

Se o seu tipo de retorno for uma matriz, retorne uma matriz vazia, caso contrário, retorne nulo.


No momento, 0 itens em uma lista são os mesmos que os da lista não atribuídos?
AnthonyWJones

3
0 itens em uma lista não são iguais a null. Ele permite que você o use em foreachdeclarações e consultas linq sem se preocupar NullReferenceException.
Darin Dimitrov

5
Estou surpreso que isso não tenha sido mais votado. Parece-me uma orientação bastante razoável.
Shashi

Bem, o contêiner vazio é apenas uma instância específica do padrão de objeto nulo. O que pode ser apropriado, não podemos dizer.
Deduplicator 21/07

Retornar uma matriz vazia quando os dados não estão disponíveis está simplesmente errado . Há uma diferença entre os dados estarem disponíveis e não conterem itens e os dados não estarem disponíveis. O retorno de uma matriz vazia nos dois casos torna impossível saber qual é o caso. Faça isso apenas para que você possa usar um foreach sem verificar se os dados existem além de tolo - o chamador deve verificar se os dados existem e uma NullReferenceException se o chamador não verificar se é bom porque expõe um bug ..
Reintegrar Monica

12

Você deve lançar uma exceção (apenas) se um contrato específico for quebrado.
No seu exemplo específico, solicitando uma UserEntity com base em um ID conhecido, isso dependeria do fato de que usuários ausentes (excluídos) sejam um caso esperado. Se sim, retorne, nullmas se não for um caso esperado, emita uma exceção.
Observe que se a função fosse chamada UserEntity GetUserByName(string name), provavelmente não seria lançada, mas retornaria nula. Em ambos os casos, retornar um UserEntity vazio seria inútil.

Para strings, matrizes e coleções, a situação geralmente é diferente. Lembro-me de algumas diretrizes do MS que os métodos devem aceitar nullcomo uma lista 'vazia', mas retornam coleções de comprimento zero em vez de null. O mesmo para seqüências de caracteres. Observe que você pode declarar matrizes vazias:int[] arr = new int[0];


Que bom que você mencionou que as strings são diferentes, já que o Google me mostrou isso quando eu decidi se deveria retornar uma string vazia.
6133 Noumenon

Strings, coleções e matrizes não são diferentes. Se a MS diz que está errado. Há uma diferença entre uma cadeia vazia e nula, e entre uma coleção vazia e nula. Nos dois casos, o primeiro representa dados existentes (de tamanho 0) e o segundo representa falta de dados. Em alguns casos, a distinção é muito importante. Por exemplo, se você procurar uma entrada em um cache, deseja saber a diferença entre os dados que estão sendo armazenados em cache, mas que estão vazios e os dados que não estão sendo armazenados em cache, para que você deve buscá-los na fonte de dados subjacente, onde pode não estar. vazio.
Reintegrar Monica

1
Você parece perder o ponto e o contexto. .Wher(p => p.Lastname == "qwerty")deve retornar uma coleção vazia, não null.
Henk Holterman

@HenkHolterman Se você puder acessar a coleção completa e aplicar um filtro que não aceita itens na coleção, uma coleção vazia é o resultado correto. Mas se a coleção completa não existir, uma coleção vazia é extremamente enganosa - nulo ou lançamento seria correto, dependendo de a situação ser normal ou excepcional. Como sua postagem não se qualificou de qual situação você estava falando (e agora você esclarece que está falando sobre a situação anterior) e como o OP estava falando sobre a última situação, tenho que discordar de você.
Reintegrar Monica

11

Essa é uma pergunta comercial, dependendo de a existência de um usuário com um Guid Id específico ser um caso de uso normal esperado para esta função ou de uma anomalia que impedirá o aplicativo de concluir com êxito qualquer função que este método esteja fornecendo ao usuário objetar para ...

Se for uma "exceção", a ausência de um usuário com esse ID impedirá que o aplicativo conclua com êxito qualquer função que esteja executando (digamos que estamos criando uma fatura para um cliente ao qual enviamos o produto ... ), essa situação deve gerar uma ArgumentException (ou alguma outra exceção personalizada).

Se um usuário ausente estiver ok, (um dos possíveis resultados normais da chamada dessa função), retorne um valor nulo ....

EDIT: (para abordar o comentário de Adam em outra resposta)

Se o aplicativo contiver vários processos de negócios, um ou mais dos quais exigem que um Usuário seja concluído com êxito e um ou mais deles possam ser concluídos com sucesso sem um usuário, a exceção deverá ser lançada na pilha de chamadas, mais perto de onde os processos de negócios que requerem um usuário estão chamando esse encadeamento de execução. Os métodos entre esse método e esse ponto (onde a exceção está sendo lançada) devem apenas comunicar que não existe usuário (nulo, booleano, qualquer que seja - este é um detalhe de implementação).

Mas se todos os processos dentro do aplicativo exigirem um usuário, eu ainda lançaria a exceção nesse método ...


-1 para o downvoter, +1 para Charles - é inteiramente uma questão de negócios e não há práticas recomendadas para isso.
21330 Austin Salonen

Está atravessando os riachos. Se é ou não uma "condição de erro", é determinado pela lógica de negócios. Como lidar com isso é uma decisão de arquitetura de aplicativo. A lógica de negócios não determinará que um nulo seja retornado, apenas que os requisitos sejam atendidos. Se a empresa está decidindo os tipos de retorno do método, eles estão envolvidos demais no aspecto técnico da implementação.
31410 Joseph Ferris

@ Joseph, o princípio básico por trás do "tratamento estruturado de exceções" é que as exceções devem ser lançadas quando os métodos não podem concluir qualquer função que eles foram codificados para implementar. Você está certo, pois se a função de negócios que este método foi codificado para implementar puder ser "concluída com êxito" (o que quer que isso signifique no Modelo de Domínio), você não precisará lançar uma exceção, poderá retornar um valor nulo. , ou uma variável booleana "FoundUser", ou qualquer outra coisa ... Como você se comunica com o método de chamada que nenhum usuário foi encontrado, torna-se um detalhe técnico da implementação.
26316 Charles Bretana

10

Pessoalmente, eu retornaria nulo, porque é assim que espero que a camada DAL / Repositório atue.

Se não existir, não retorne nada que possa ser interpretado como uma busca bem-sucedida de um objeto, nullfunciona lindamente aqui.

O mais importante é ser consistente em toda a sua camada DAL / Repos, para que você não fique confuso sobre como usá-lo.


7

Eu tendem a

  • return nullse o ID do objeto não existir, quando não for conhecido previamente se ele deve existir.
  • throwse o ID do objeto não existir quando deveria existir.

Eu diferencio esses dois cenários com esses três tipos de métodos. Primeiro:

Boolean TryGetSomeObjectById(Int32 id, out SomeObject o)
{
    if (InternalIdExists(id))
    {
        o = InternalGetSomeObject(id);

        return true;
    }
    else
    {
        return false;
    }
}

Segundo:

SomeObject FindSomeObjectById(Int32 id)
{
    SomeObject o;

    return TryGetObjectById(id, out o) ? o : null;
}

Terceiro:

SomeObject GetSomeObjectById(Int32 id)
{
    SomeObject o;

    if (!TryGetObjectById(id, out o))
    {
        throw new SomeAppropriateException();
    }

    return o;
}

Você quer dizer que outnãoref
Matt Ellen

@ Matt: Sim, senhor, eu certamente faço! Fixo.
Johann Gerell 19/10/10

2
Realmente, esta é a única resposta única e, portanto, a verdade total e única! :) Sim, depende das suposições com base nas quais o método é chamado ... Então, primeiro esclareça essas suposições e escolha a combinação correta das opções acima. Tive que rolar muito para baixo até chegar aqui :) +100
yair

Esse parece ser o padrão a ser usado, exceto que ele não suporta métodos assíncronos. Eu referenciei esta resposta e adicionei uma solução assíncrona com literais de tupla -> aqui
ttugates

6

Ainda outra abordagem envolve a transmissão de um objeto ou delegado de retorno de chamada que operará no valor. Se um valor não for encontrado, o retorno de chamada não será chamado.

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
}

Isso funciona bem quando você deseja evitar verificações nulas em todo o seu código e quando não encontrar um valor não é um erro. Você também pode fornecer um retorno de chamada para quando nenhum objeto for encontrado, se você precisar de algum processamento especial.

public void GetUserById(Guid id, UserCallback callback, NotFoundCallback notFound)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
    else
        notFound(); // or notFound.Call();
}

A mesma abordagem usando um único objeto pode ser semelhante a:

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback.Found(userEntity);
    else
        callback.NotFound();
}

Do ponto de vista do design, eu realmente gosto dessa abordagem, mas tem a desvantagem de tornar o site de chamada mais volumoso em idiomas que não oferecem suporte imediato a funções de primeira classe.


Interessante. Quando você começou a falar sobre delegados, comecei imediatamente a me perguntar se expressões Lambda poderiam ser usadas aqui.
7wp 08/11/2009

Sim! Pelo que entendi, a sintaxe lambda do C # 3.0 e up é basicamente açúcar sintático para delegados anônimos. Da mesma forma em Java, sem a boa sintaxe lambda ou delegada anônima, você pode simplesmente criar uma classe anônima. É um pouco mais feio, mas pode ser realmente útil. Suponho que estes dias, o meu C # exemplo poderia ter usado Func <UserEntity> ou algo parecido, em vez de um delegado nomeado, mas a última C # projeto eu estava em ainda estava usando a versão 2.
Marc

+1 Eu gosto dessa abordagem. Um problema, porém, é que não é convencional e aumenta ligeiramente a barreira de entrada para uma base de código.
timoxley

4

Usamos o CSLA.NET e consideramos que uma busca de dados com falha deve retornar um objeto "vazio". Isso é realmente muito irritante, pois exige a convenção de verificar se obj.IsNewprefere obj == null.

Como um pôster anterior mencionado, os valores de retorno nulos farão com que o código falhe imediatamente, reduzindo a probabilidade de problemas furtivos causados ​​por objetos vazios.

Pessoalmente, acho que nullé mais elegante.

É um caso muito comum, e estou surpreso que as pessoas aqui pareçam surpreendidas por ele: em qualquer aplicativo Web, os dados geralmente são buscados usando um parâmetro de string de consulta, que obviamente pode ser confundido, exigindo que o desenvolvedor lide com incidências de "não encontrado" "

Você pode lidar com isso da seguinte maneira:

if (User.Exists (id)) {
  this.User = User.Fetch (id);
} outro {
  Response.Redirect ("~ / notfound.aspx");
}

... mas é sempre uma chamada extra para o banco de dados, o que pode ser um problema nas páginas de alto tráfego. Enquanto que:

this.User = User.Fetch (id);

if (this.User == null) {
  Response.Redirect ("~ / notfound.aspx");
}

... requer apenas uma chamada.


4

Eu prefiro null, pois é compatível com o operador de coalescência nula ( ??).


4

Eu diria que retorne nulo em vez de um objeto vazio.

Mas na instância específica que você mencionou aqui, você está procurando um usuário pelo ID do usuário, que é a chave para esse usuário. Nesse caso, eu provavelmente gostaria de lançar uma exceção se nenhuma instância da instância do usuário for encontrada .

Esta é a regra que geralmente sigo:

  • Se nenhum resultado encontrado em uma localização por operação de chave primária, jogue ObjectNotFoundException.
  • Se nenhum resultado encontrado em uma localização por qualquer outro critério, retorne nulo.
  • Se nenhum resultado encontrado em uma descoberta por um critério não chave que pode retornar vários objetos, retornará uma coleção vazia.

Por que você lançaria uma exceção em algum desses casos? Às vezes, os usuários não existem no banco de dados e esperamos que isso não aconteça. Não é um comportamento excepcional.
Siride

3

Isso varia de acordo com o contexto, mas geralmente retornarei nulo se estiver procurando por um objeto específico (como no seu exemplo) e retornarei uma coleção vazia se estiver procurando por um conjunto de objetos, mas não houver nenhum.

Se você cometeu um erro no seu código e retornar nulos leva a exceções de ponteiros nulos, quanto mais cedo você perceber isso, melhor. Se você retornar um objeto vazio, o uso inicial dele poderá funcionar, mas você poderá obter erros posteriormente.


+1 Eu estava questionando a mesma lógica que você está dizendo aqui, e foi por isso que postei a pergunta para ver quais seriam as opiniões dos outros
7wp 26/10/2009

3

O melhor nesse caso retorna "nulo", caso não exista esse usuário. Também torne seu método estático.

Editar:

Normalmente, métodos como este são membros de alguma classe "Usuário" e não têm acesso aos membros da instância. Nesse caso, o método deve ser estático; caso contrário, você deve criar uma instância de "Usuário" e chamar o método GetUserById que retornará outra instância de "Usuário". Concordo que isso é confuso. Mas se o método GetUserById for membro de alguma classe "DatabaseFactory" - não há problema em deixá-lo como membro da instância.


Posso perguntar por que gostaria de tornar meu método estático? E se eu quiser usar a Injeção de Dependência?
7wp 26/10/09

Ok, agora eu entendo sua lógica. Mas eu continuo com o padrão Repository e gosto de usar injeção de dependência para meus repositórios, portanto, não posso usar métodos estáticos. Mas +1 para sugerir para retornar nulo :)
7wp

3

Eu pessoalmente retorno uma instância padrão do objeto. O motivo é que espero que o método retorne zero a muitos ou zero a um (dependendo da finalidade do método). A única razão pela qual seria um estado de erro de qualquer tipo, usando essa abordagem, é se o método não retornou nenhum objeto (s) e sempre foi esperado (em termos de retorno um para muitos ou singular).

Quanto à suposição de que essa é uma questão de domínio de negócios - simplesmente não a vejo desse lado da equação. A normalização de tipos de retorno é uma questão válida de arquitetura de aplicativo. No mínimo, está sujeito a padronização nas práticas de codificação. Duvido que haja um usuário corporativo que diga "no cenário X, apenas dê um nulo".


+1 Gosto da visão alternativa do problema. Então, basicamente, você está dizendo que qualquer abordagem que eu escolher deve ficar bem, desde que o método seja consistente em todo o aplicativo?
7wp 26/10/09

1
Essa é a minha crença. Eu acho que a consistência é extremamente importante. Se você faz as coisas de várias maneiras em vários lugares, isso gera um risco maior de novos bugs. Pessoalmente, adotamos a abordagem de objeto padrão, porque ela funciona bem com o padrão Essence que usamos em nosso modelo de domínio. Temos um único método de extensão genérico que podemos testar em relação a todos os objetos de domínio para nos informar se está preenchido ou não, para que saibamos que qualquer DO pode ser testado com uma chamada de objectname.IsDefault () - evitando diretamente verificações de igualdade .
31410 Joseph Ferris

3

Nos nossos Objetos de Negócios, temos 2 métodos Get principais:

Para manter as coisas simples no contexto ou você questiona, elas seriam:

// Returns null if user does not exist
public UserEntity GetUserById(Guid userId)
{
}

// Returns a New User if user does not exist
public UserEntity GetNewOrExistingUserById(Guid userId)
{
}

O primeiro método é usado ao obter entidades específicas, o segundo método é usado especificamente ao adicionar ou editar entidades em páginas da web.

Isso nos permite ter o melhor dos dois mundos no contexto em que são usados.


3

Eu sou um estudante de TI francês, então desculpe meu inglês ruim. Nas nossas aulas, somos informados de que esse método nunca deve retornar nulo, nem um objeto vazio. O usuário desse método deve verificar primeiro se o objeto que ele está procurando existe antes de tentar obtê-lo.

Usando Java, somos solicitados a adicionar um assert exists(object) : "You shouldn't try to access an object that doesn't exist";no início de qualquer método que possa retornar nulo, para expressar a "pré-condição" (não sei qual é a palavra em inglês).

Na IMO, isso realmente não é fácil de usar, mas é isso que estou usando, esperando por algo melhor.


1
Obrigado pela sua resposta. Mas não gosto da ideia de verificar primeiro se existe. O motivo é que gera uma consulta adicional ao banco de dados. Em um aplicativo que está sendo acessado por milhões de pessoas em um dia, isso pode resultar em perda drástica de desempenho.
7wp 28/10/09

1
Uma vantagem é que a verificação de existência é apropriadamente resumo: se (userexists) é ligeiramente mais legível, mais próximo do domínio do problema, e menos 'computery' do que: if (usuário == null)
timoxley

E eu argumentaria que 'if (x == null)' é um padrão de décadas que, se você não o viu antes, não escreve código há muito tempo (e deve se acostumar com isso como está em milhões de linhas de código). "Computação"? Estamos falando de acesso ao banco de dados ...
Lloyd Sargent

3

Se o caso do usuário não sendo encontrado surge muitas vezes o suficiente, e você quer lidar com isso de várias maneiras, dependendo da circunstância (às vezes jogando uma exceção, por vezes substituindo um usuário vazio) você também pode usar algo parecido com 's F # Optionde ou Haskell Maybetipo , que separa explicitamente o caso 'sem valor' de 'encontrado algo!'. O código de acesso ao banco de dados pode ficar assim:

public Option<UserEntity> GetUserById(Guid userId)
{
 //Imagine some code here to access database.....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return Option<UserEntity>.Nothing; 
 else
    return Option.Just(existingUserEntity);
}

E ser usado assim:

Option<UserEntity> result = GetUserById(...);
if (result.IsNothing()) {
    // deal with it
} else {
    UserEntity value = result.GetValue();
}

Infelizmente, todo mundo parece ter um tipo como esse próprio.


2

Eu normalmente retorno nulo. Ele fornece um mecanismo rápido e fácil para detectar se algo estragou tudo sem gerar exceções e usando toneladas de tentativa / captura em todo o lugar.


2

Para os tipos de coleção, eu retornaria uma coleção vazia, para todos os outros tipos, prefiro usar os padrões NullObject para retornar um objeto que implementa a mesma interface que a do tipo de retorno. para obter detalhes sobre o padrão, consulte o texto do link

Usando o padrão NullObject, isso seria: -

public UserEntity GetUserById(Guid userId)

{// Imagine algum código aqui para acessar o banco de dados .....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return new NullUserEntity(); //Should I be doing this here instead? return new UserEntity();  
 else
    return existingUserEntity;

}

class NullUserEntity: IUserEntity { public string getFirstName(){ return ""; } ...} 

2

Para colocar o que os outros disseram de maneira conciliatória ...

Exceções são para circunstâncias excepcionais

Se esse método for pura camada de acesso a dados, eu diria que, dado algum parâmetro incluído em uma instrução select, seria de esperar que eu não encontrasse nenhuma linha para construir um objeto e, portanto, retornar nulo seria aceitável, pois é lógica de acesso a dados.

Por outro lado, se eu esperasse que meu parâmetro refletisse uma chave primária e só devesse recuperar uma linha, se recuperasse mais de uma, lançaria uma exceção. 0 está ok para retornar nulo, 2 não está.

Agora, se eu tivesse algum código de login verificado em um provedor LDAP e em um banco de dados para obter mais detalhes e esperava que eles estivessem sincronizados o tempo todo, talvez eu lance a exceção. Como outros disseram, são regras de negócios.

Agora vou dizer que é uma regra geral . Há momentos em que você pode querer quebrar isso. No entanto, minha experiência e experimentos com C # (muito disso) e Java (um pouco disso) me ensinaram que é muito mais caro em termos de desempenho lidar com exceções do que lidar com problemas previsíveis por meio da lógica condicional. Estou falando da ordem de 2 ou 3 ordens de magnitude mais caras em alguns casos. Portanto, se for possível que seu código termine em um loop, eu recomendaria retornar nulo e testá-lo.


2

Perdoe meu pseudo-php / código.

Eu acho que realmente depende do uso pretendido do resultado.

Se você pretende editar / modificar o valor retornado e salvá-lo, retorne um objeto vazio. Dessa forma, você pode usar a mesma função para preencher dados em um objeto novo ou existente.

Digamos que eu tenho uma função que pega uma chave primária e uma matriz de dados, preenche a linha com dados e salva o registro resultante no banco de dados. Como pretendo preencher o objeto com meus dados de qualquer maneira, pode ser uma grande vantagem recuperar um objeto vazio do getter. Dessa forma, eu posso executar operações idênticas em ambos os casos. Você usa o resultado da função getter, não importa o quê.

Exemplo:

function saveTheRow($prim_key, $data) {
    $row = getRowByPrimKey($prim_key);

    // Populate the data here

    $row->save();
}

Aqui podemos ver que a mesma série de operações manipula todos os registros desse tipo.

No entanto, se a intenção final do valor de retorno for ler e fazer algo com os dados, retornarei nulo. Dessa forma, posso determinar rapidamente se não houve retorno de dados e exibir a mensagem apropriada para o usuário.

Normalmente, capturarei exceções em minha função que recuperam os dados (para que eu possa registrar mensagens de erro, etc ...) e depois retornarei nulo diretamente da captura. Geralmente, não importa para o usuário final qual é o problema; portanto, acho melhor encapsular meu registro / processamento de erros diretamente na função que obtém os dados. Se você estiver mantendo uma base de código compartilhada em qualquer empresa grande, isso é especialmente benéfico, porque você pode forçar o registro / tratamento de erros adequado até mesmo no programador mais preguiçoso.

Exemplo:

function displayData($row_id) {
    // Logging of the error would happen in this function
    $row = getRow($row_id);
    if($row === null) {
        // Handle the error here
    }

    // Do stuff here with data
}

function getRow($row_id) {
 $row = null;
 try{
     if(!$db->connected()) {
   throw excpetion("Couldn't Connect");
  }

  $result = $db->query($some_query_using_row_id);

  if(count($result) == 0 ) {
   throw new exception("Couldn't find a record!");
  }

  $row = $db->nextRow();

 } catch (db_exception) {
  //Log db conn error, alert admin, etc...
  return null; // This way I know that null means an error occurred
 }
 return $row;
}

Essa é a minha regra geral. Até agora funcionou bem.


2

Pergunta interessante e acho que não há resposta "certa", pois sempre depende da responsabilidade do seu código. Seu método sabe se nenhum dado encontrado é um problema ou não? Na maioria dos casos, a resposta é "não" e é por isso que retornar nulo e permitir que o chamador lide com a situação é perfeita.

Talvez uma boa abordagem para distinguir métodos de lançamento de métodos de retorno nulo seja encontrar uma convenção em sua equipe: Métodos que dizem que "recebem" algo devem gerar uma exceção se não houver nada para obter. Os métodos que podem retornar nulos podem ter nomes diferentes, talvez "Localizar ...".


+1 Gosto da ideia de usar uma convenção de nomenclatura uniforme para sinalizar ao programador como essa função deve ser consumida.
7wp 28/10/09

1
De repente, eu reconheço que isso é o LINQ faz: considerar First (...) vs. FirstOrDefault (...)
Marc Wittke

2

Se o objeto retornado for algo que pode ser iterado, eu retornaria um objeto vazio, para que não precise testar primeiro o nulo.

Exemplo:

bool IsAdministrator(User user)
{
    var groupsOfUser = GetGroupsOfUser(user);

    // This foreach would cause a run time exception if groupsOfUser is null.
    foreach (var groupOfUser in groupsOfUser) 
    {
        if (groupOfUser.Name == "Administrators")
        {
            return true;
        }
    }

    return false;
}

2

Eu gosto de não retornar nulo de nenhum método, mas de usar o tipo funcional Option. Os métodos que não podem retornar resultado retornam uma opção vazia, em vez de nula.

Além disso, esses métodos que não podem retornar resultados devem indicar isso por meio de seus nomes. Normalmente, coloquei Try ou TryGet ou TryFind no início do nome do método para indicar que ele pode retornar um resultado vazio (por exemplo, TryFindCustomer, TryLoadFile, etc.).

Isso permite que o chamador aplique técnicas diferentes, como pipelining de coleção (consulte Pipeline de coleção de Martin Fowler ) no resultado.

Aqui está outro exemplo em que retornar Option em vez de null é usado para reduzir a complexidade do código: Como reduzir a complexidade ciclomática: Tipo funcional da opção


1
Eu escrevi uma resposta, posso ver que é semelhante à sua enquanto eu rolei para cima e concordo que você pode implementar um tipo de opção com uma coleção genérica com 0 ou 1 elementos. Obrigado pelos links adicionais.
Gabriel P.

1

Mais carne para moer: digamos que meu DAL retorne um NULL para GetPersonByID, conforme recomendado por alguns. O que minha BLL (bastante fina) deve fazer se receber um NULL? Passar esse NULL para cima e deixar o consumidor final se preocupar com isso (nesse caso, uma página ASP.Net)? Que tal ter o BLL lançar uma exceção?

O BLL pode estar sendo usado pelo ASP.Net e Win App, ou outra biblioteca de classes - acho injusto esperar que o consumidor final "intrinsecamente" saiba que o método GetPersonByID retorna um valor nulo (a menos que tipos nulos sejam usados, eu acho )

Minha opinião (pelo que vale a pena) é que meu DAL retorne NULL se nada for encontrado. PARA ALGUNS OBJETOS, tudo bem - poderia ser uma lista de 0: muitas coisas, então não ter nada é bom (por exemplo, uma lista de livros favoritos). Nesse caso, minha BLL retorna uma lista vazia. Para a maioria das coisas de uma única entidade (por exemplo, usuário, conta, fatura), se eu não tiver uma, isso é definitivamente um problema e uma exceção cara. No entanto, como recuperar um usuário por um identificador exclusivo fornecido anteriormente pelo aplicativo sempre deve retornar um usuário, a exceção é uma exceção "adequada", como é excepcional. O consumidor final da BLL (ASP.Net, por exemplo) só espera que as coisas sejam exageradas; portanto, um Manipulador de Exceção Unhandled será usado em vez de agrupar todas as chamadas para GetPersonByID em um bloco try-catch.

Se houver um problema evidente em minha abordagem, informe-me, pois estou sempre interessado em aprender. Como outros pôsteres disseram, as exceções são caras e a abordagem de "verificar primeiro" é boa, mas as exceções devem ser exatamente isso - excepcionais.

Estou gostando deste post, muitas boas sugestões para cenários "depende" :-)


E, é claro, hoje me deparei com um cenário em que retornarei NULL da minha BLL ;-) Dito isso, eu ainda poderia lançar uma exceção e usar try / catch na minha classe consumidora, mas ainda tenho um problema : como minha classe consumidora sabe usar um try / catch, semelhante como eles sabem procurar NULL?
55630 Mike Kingscott #

Você pode documentar que um método lança uma exceção por meio da marca de documento @throws e documenta o fato de que pode retornar nulo na marca de documento @return.
timoxley

1

Eu acho que as funções não devem retornar nulas, para a saúde da sua base de código. Eu posso pensar em alguns motivos:

Haverá uma grande quantidade de cláusulas de guarda tratando referência nula if (f() != null).

O que é null, é uma resposta aceita ou um problema? Nulo é um estado válido para um objeto específico? (imagine que você é um cliente para o código). Quero dizer que todos os tipos de referência podem ser nulos, mas deveriam?

Se nullficar por perto, quase sempre dará algumas exceções inesperadas ao NullRef de tempos em tempos, à medida que sua base de código cresce.

Existem algumas soluções tester-doer patternou a implementação option typeda programação funcional.


0

Estou perplexo com o número de respostas (em toda a web) que dizem que você precisa de dois métodos: um método "IsItThere ()" e um método "GetItForMe ()", e isso leva a uma condição de corrida. O que há de errado com uma função que retorna nula, atribuindo-a a uma variável e verificando a variável como Nula, tudo em um teste? Meu antigo código C foi salpicado com

if (NULL! = (variável = função (argumentos ...))) {

Então você obtém o valor (ou nulo) em uma variável e o resultado de uma só vez. Este idioma foi esquecido? Por quê?


0

Eu concordo com a maioria das postagens aqui, que tendem para null.

Meu raciocínio é que a geração de um objeto vazio com propriedades não anuláveis ​​pode causar erros. Por exemplo, uma entidade com uma int IDpropriedade teria um valor inicial de ID = 0, que é um valor totalmente válido. Se esse objeto, sob alguma circunstância, for salvo no banco de dados, seria uma coisa ruim.

Para qualquer coisa com um iterador, eu sempre usaria a coleção vazia. Algo como

foreach (var eachValue in collection ?? new List<Type>(0))

é o cheiro do código na minha opinião. As propriedades da coleção nunca devem ser nulas.

Um caso de ponta é String. Muitas pessoas dizem que isso String.IsNullOrEmptynão é realmente necessário, mas nem sempre é possível distinguir entre uma string vazia e nula. Além disso, alguns sistemas de banco de dados (Oracle) não fazem distinção entre eles ( ''são armazenados como DBNULL), então você é forçado a lidar com eles igualmente. A razão para isso é que a maioria dos valores de string é proveniente da entrada do usuário ou de sistemas externos, enquanto nem as caixas de texto nem a maioria dos formatos de troca têm representações diferentes para ''e null. Portanto, mesmo que o usuário deseje remover um valor, ele não poderá fazer nada além de limpar o controle de entrada. Além disso, a distinção entre nvarcharcampos de banco de dados anuláveis ​​e não anuláveis é mais do que questionável, se o seu DBMS não for Oracle - um campo obrigatório que permite''é estranho, sua interface do usuário nunca permitiria isso; portanto, suas restrições não são mapeadas. Portanto, a resposta aqui, na minha opinião, é lidar com eles igualmente, sempre.

Em relação à sua pergunta sobre exceções e desempenho: Se você lançar uma exceção que não pode lidar completamente na lógica do seu programa, precisará abortar, em algum momento, o que o seu programa estiver fazendo e pedir ao usuário que refaça o que acabou de fazer. Nesse caso, a penalidade de desempenho de a catché realmente a menor das suas preocupações - ter que perguntar ao usuário é o elefante na sala (o que significa re-renderizar toda a interface do usuário ou enviar algum HTML pela Internet). Portanto, se você não seguir o antipadrão de " Fluxo de programa com exceções ", não se preocupe, basta lançar um se fizer sentido. Mesmo em casos limítrofes, como "Exceção de validação", o desempenho não é realmente um problema, pois é necessário perguntar novamente ao usuário, em qualquer caso.


0

Um padrão TryGet assíncrono:

Para métodos síncronos, acredito que a resposta de @Johann Gerell é o padrão a ser usado em todos os casos.

No entanto, o padrão TryGet com o outparâmetro não funciona com métodos Async.

Com as Tuplas Literais do C # 7, agora você pode fazer isso:

async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id)
{
    if (InternalIdExists(id))
    {
        o = await InternalGetSomeObjectAsync(id);

        return (true, o);
    }
    else
    {
        return (false, default(SomeObject));
    }
}
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.