Referências não anuláveis ​​em C # 8 e o padrão Try

23

Há um padrão nas classes C # exemplificadas por Dictionary.TryGetValuee int.TryParse: um método que retorna um valor booleano indicando o sucesso de uma operação e um parâmetro out contendo o resultado real; se a operação falhar, o parâmetro out será definido como nulo.

Vamos supor que estou usando referências não anuláveis ​​em C # 8 e quero escrever um método TryParse para minha própria classe. A assinatura correta é esta:

public static bool TryParse(string s, out MyClass? result);

Como o resultado é nulo no caso falso, a variável out deve ser marcada como anulável.

No entanto, o padrão Try é geralmente usado assim:

if (MyClass.TryParse(s, out var result))
{
  // use result here
}

Como apenas insiro a ramificação quando a operação é bem-sucedida, o resultado nunca deve ser nulo nessa ramificação. Mas como o marquei como nulo, agora tenho que verificar isso ou usar !para substituir:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // compiler warning, could be null
  Console.WriteLine("Look: {0}", result!.SomeProperty); // need override
}

Isso é feio e um pouco anti-econômico.

Por causa do padrão de uso típico, tenho outra opção: mentir sobre o tipo de resultado:

public static bool TryParse(string s, out MyClass result) // not nullable
{
   // Happy path sets result to non-null and returns true.
   // Error path does this:
   result = null!; // override compiler complaint
   return false;
}

Agora, o uso típico se torna melhor:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // no warning
}

mas o uso atípico não recebe o aviso que deveria:

else
{
  Console.WriteLine("Fail: {0}", result.SomeProperty);
  // Yes, result is in scope here. No, it will never be non-null.
  // Yes, it will throw. No, the compiler won't warn about it.
}

Agora não tenho certeza de qual caminho seguir aqui. Existe uma recomendação oficial da equipe de idiomas C #? Existe algum código CoreFX já convertido em referências não anuláveis ​​que possa me mostrar como fazer isso? ( Procurei TryParsemétodos. IPAddressÉ uma classe que possui uma, mas não foi convertida na ramificação principal do corefx.)

E como código genérico como Dictionary.TryGetValuelida com isso? (Possivelmente com um MaybeNullatributo especial do que encontrei.) O que acontece quando instanciamos um Dictionarycom um tipo de valor não nulo?

Sebastian Redl
fonte
Não tentei (e é por isso que não estou escrevendo isso como resposta), mas com o novo recurso de correspondência de padrões das instruções switch, acho que uma opção é simplesmente retornar a referência nula (sem Try-pattern, return MyClass?) e faça uma troca com ae case MyClass myObj:(opcional) case null:.
Filip Milovanović
+1 Gosto muito dessa pergunta e, quando tive que trabalhar com isso, sempre usei uma verificação nula extra em vez da substituição - que sempre parecia um pouco desnecessária e deselegante, mas nunca estava no código crítico de desempenho então eu apenas deixo pra lá. Seria bom ver se existe uma maneira mais limpa de lidar com isso!
BrianH 25/02

Respostas:

10

O padrão bool / out-var não funciona bem com tipos de referência anuláveis, como você descreve. Portanto, em vez de combater o compilador, use o recurso para simplificar as coisas. Jogue os recursos aprimorados de correspondência de padrões do C # 8 e você poderá tratar uma referência nula como um "talvez tipo de homem pobre":

public static MyClass? TryParse(string s) => 



if (TryParse(someString) is {} myClass)
{
    // myClass wasn't null, we are good to use it
}

Dessa forma, você evita mexer nos outparâmetros e não precisa brigar com o compilador misturando-o nullcom referências não anuláveis.

E como código genérico como Dictionary.TryGetValuelida com isso?

Nesse ponto, esse "tipo de pobre homem talvez" cai. O desafio que você enfrentará é que, ao usar tipos de referência anuláveis ​​(NRT), o compilador tratará Foo<T>como não anulável. Mas tente alterá-lo para Foo<T?>e ele desejará que as Trestrições a uma classe ou estrutura como tipos de valor anuláveis ​​sejam algo muito diferente do ponto de vista do CLR. Há uma variedade de soluções alternativas para isso:

  1. Não ative o recurso NRT,
  2. Comece a usar default(junto com !) os outparâmetros, mesmo que seu código não esteja nulo,
  3. Use uma verdadeira Maybe<T>tipo que o valor de retorno, que é depois nunca nulle envolve que boole out Tem HasValuee Valuepropriedades ou algo assim,
  4. Use uma tupla:
public static (bool success, T result) TryParse<T>(string s) => 


if (TryParse<MyClass>(someString) is (true, var result))
{
    // result is valid here, as success is true
}

Pessoalmente, sou a favor do uso, Maybe<T>mas o suporte a uma desconstrução para que ele possa corresponder ao padrão como uma tupla, como em 4 acima.

David Arno
fonte
2
TryParse(someString) is {} myClass- Essa sintaxe levará algum tempo para se acostumar, mas eu gosto da ideia.
Sebastian Redl
TryParse(someString) is var myClassparece mais fácil para mim.
Olivier Jacot-Descombes
2
@ OlivierJacot-Descombes pode parecer mais fácil ... mas não vai funcionar. O padrão do carro sempre corresponde, portanto x is var ysempre será verdadeiro, seja xnulo ou não.
David Arno
15

Se você chegar um pouco tarde, como eu, a equipe do .NET abordou isso através de vários atributos de parâmetro, como MaybeNullWhen(returnValue: true)no System.Diagnostics.CodeAnalysisespaço que você pode usar para o padrão try.

Por exemplo:

como código genérico como Dictionary.TryGetValue lida com isso?

bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)] out TValue value);

o que significa que você é gritado se não procurar um true

// This is okay:
if(myDictionary.TryGetValue("cheese", out var result))
{
  var more = result * 42;
}

// But this is not:
_ = myDictionary.TryGetValue("cheese", out var result);
var more = result * 42;
// "CS8602: Dereference of a potentially null reference"

Detalhes adicionais:

Nick Darvey
fonte
3

Eu não acho que há um conflito aqui.

sua objeção a

public static bool TryParse(string s, out MyClass? result);

é

Como apenas insiro a ramificação quando a operação é bem-sucedida, o resultado nunca deve ser nulo nessa ramificação.

No entanto, de fato, nada impede a atribuição de null ao parâmetro out nas funções TryParse do estilo antigo.

por exemplo.

MyJsonObject.TryParse("null", out obj) //sets obj to a null MyJsonObject and returns true

O aviso dado ao programador quando eles usam o parâmetro out sem verificação está correto. Você deveria estar verificando!

Haverá muitos casos em que você será forçado a retornar tipos anuláveis, onde a ramificação principal do código retornará um tipo não anulável. O aviso está aí para ajudá-lo a torná-los explícitos. ie

MyClass? x = (new List<MyClass>()).FirstOrDefault(i=>i==1);

A maneira não anulável de codificá-lo lançará uma exceção em que teria havido um nulo. Seja sua análise, obtenção ou inicialização

MyClass x = (new List<MyClass>()).First(i=>i==1);
Ewan
fonte
Não acho que FirstOrDefaultpossa ser comparado, porque a nulidade de seu valor de retorno é o sinal principal. Nos TryParsemétodos, o parâmetro out não sendo nulo se o valor de retorno for verdadeiro faz parte do contrato do método.
Sebastian Redl
não faz parte do contrato. a única coisa que é garantida é que algo é atribuído ao parâmetro out
Ewan
É o comportamento que espero de um TryParsemétodo. Se IPAddress.TryParsealguma vez retornou verdadeiro, mas não atribuiu um valor não nulo ao seu parâmetro out, eu o reportaria como um bug.
Sebastian Redl
Sua expectativa é compreensível, mas não é imposta pelo compilador. Então, com certeza, a especificação para IpAddress pode dizer que nunca retorna verdadeiro e nulo, mas meu exemplo JsonObject mostra um caso em que o retorno nulo pode estar correto
Ewan
"Sua expectativa é compreensível, mas não é imposta pelo compilador." - Eu sei, mas minha pergunta é como melhor escrever meu código para expressar o contrato que tenho em mente, não qual é o contrato de outro código.
Sebastian Redl