C # 8 referencias no anulables y el patrón Try

23

Hay un patrón en las clases de C # ejemplificado por Dictionary.TryGetValuey int.TryParse: un método que devuelve un valor booleano que indica el éxito de una operación y un parámetro de salida que contiene el resultado real; Si la operación falla, el parámetro de salida se establece en nulo.

Supongamos que estoy usando referencias no nulables de C # 8 y quiero escribir un método TryParse para mi propia clase. La firma correcta es esta:

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

Como el resultado es nulo en el caso falso, la variable de salida debe marcarse como nulable.

Sin embargo, el patrón Try generalmente se usa así:

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

Como solo entro en la rama cuando la operación tiene éxito, el resultado nunca debe ser nulo en esa rama. Pero debido a que lo marqué como anulable, ahora tengo que verificar eso o usar !para anular:

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
}

Esto es feo y un poco poco ergonómico.

Debido al patrón de uso típico, tengo otra opción: mentir sobre el 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;
}

Ahora el uso típico se vuelve más agradable:

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

pero el uso atípico no recibe la advertencia que debería:

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.
}

Ahora no estoy seguro de qué camino tomar aquí. ¿Hay alguna recomendación oficial del equipo de lenguaje C #? ¿Hay algún código CoreFX ya convertido en referencias no anulables que pueda mostrarme cómo hacer esto? (Fui a buscar TryParsemétodos. IPAddressEs una clase que tiene uno, pero no se ha convertido en la rama maestra de corefx).

¿Y cómo se ocupa esto de un código genérico Dictionary.TryGetValue? (Posiblemente con un MaybeNullatributo especial de lo que encontré.) ¿Qué sucede cuando instancia una Dictionarycon un tipo de valor no anulable?

Sebastian Redl
fuente
No lo intenté (por lo que no estoy escribiendo esto como respuesta), pero con la nueva característica de coincidencia de patrones de las declaraciones de cambio, supongo que una opción es simplemente devolver la referencia anulable (sin patrón Try, volver MyClass?), y hacer un cambio con un case MyClass myObj:y (una opción todos) case null:.
Filip Milovanović
+1 Realmente me gusta esta pregunta, y cuando tuve que trabajar con esto siempre usé una verificación nula adicional en lugar de la anulación, que siempre me pareció un poco innecesaria e inelegante, pero nunca fue en el código crítico de rendimiento así que lo dejé pasar ¡Sería bueno ver si hay una forma más limpia de manejarlo!
BrianH

Respuestas:

10

El patrón bool / out-var no funciona bien con los tipos de referencia anulables, como usted describe. Entonces, en lugar de luchar contra el compilador, use la función para simplificar las cosas. Agregue las características mejoradas de coincidencia de patrones de C # 8 y puede tratar una referencia anulable como "tipo de hombre pobre":

public static MyClass? TryParse(string s) => 



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

De esa manera, evita jugar con los outparámetros y no tiene que luchar con el compilador por mezclar nullcon referencias no anulables.

¿Y cómo se ocupa esto de un código genérico Dictionary.TryGetValue?

En este punto, ese "tipo de hombre pobre quizás" se cae. El desafío que enfrentará es que cuando use tipos de referencia anulables (NRT), el compilador lo tratará Foo<T>como no anulable. Pero intente cambiarlo Foo<T?>y querrá Trestringirlo a una clase o estructura ya que los tipos de valores anulables son algo muy diferente desde el punto de vista del CLR. Hay una variedad de soluciones para esto:

  1. No habilite la función NRT,
  2. Comience a usar default(junto con !) para los outparámetros, aunque su código se registre sin nulos
  3. Utilice un verdadero Maybe<T>tipo que el valor de retorno, que luego nunca nully se envuelve de que booly out Ten HasValuey Valuepropiedades o algo así,
  4. Usa una 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
}

Personalmente, estoy a favor de usarlo, Maybe<T>pero que sea compatible con una deconstrucción para que pueda coincidir con el patrón como una tupla como en 4, arriba.

David Arno
fuente
2
TryParse(someString) is {} myClass- Esta sintaxis llevará un tiempo acostumbrarse, pero me gusta la idea.
Sebastian Redl
TryParse(someString) is var myClassme parece más fácil
Olivier Jacot-Descombes
2
@ OlivierJacot-Descombes Puede parecer más fácil ... pero no funcionará. El patrón del automóvil siempre coincide, por x is var ylo que siempre será cierto, sea xnulo o no.
David Arno
15

Si llega a esto un poco tarde, como yo, resulta que el equipo de .NET lo abordó a través de un conjunto de atributos de parámetros como MaybeNullWhen(returnValue: true)en el System.Diagnostics.CodeAnalysisespacio que puede usar para el patrón de prueba.

Por ejemplo:

¿Cómo maneja esto un código genérico como Dictionary. TryGetValue?

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

lo que significa que te gritan si no compruebas si hay un 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"

Más detalles:

Nick Darvey
fuente
3

No creo que haya un conflicto aquí.

tu objeción a

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

es

Como solo entro en la rama cuando la operación tiene éxito, el resultado nunca debe ser nulo en esa rama.

Sin embargo, de hecho, no hay nada que impida la asignación de nulo al parámetro out en las funciones TryParse de estilo antiguo.

p.ej.

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

La advertencia dada al programador cuando utiliza el parámetro de salida sin verificar es correcta. ¡Deberías estar comprobando!

Habrá un montón de casos en los que se verá obligado a devolver tipos anulables donde la rama principal del código devuelve un tipo no anulable. La advertencia está ahí para ayudarlo a hacer esto explícito. es decir.

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

La forma no anulable de codificarlo arrojará una excepción donde hubiera habido un valor nulo. Ya sea que esté analizando, obteniendo o iniciando

MyClass x = (new List<MyClass>()).First(i=>i==1);
Ewan
fuente
No creo que FirstOrDefaultse pueda comparar, porque la nulidad de su valor de retorno es la señal principal. En los TryParsemétodos, el parámetro out no es nulo si el valor de retorno es verdadero es parte del contrato del método.
Sebastian Redl
No es parte del contrato. lo único seguro es que algo está asignado al parámetro out
Ewan
Es el comportamiento que espero de un TryParsemétodo. Si IPAddress.TryParsealguna vez devuelve verdadero pero no asignó no nulo a su parámetro out, lo reportaría como un error.
Sebastian Redl
Su expectativa es comprensible pero el compilador no la exige. Entonces, la especificación de IpAddress podría decir que nunca devuelve verdadero y nulo, pero mi ejemplo de JsonObject muestra un caso en el que devolver nulo podría ser correcto
Ewan
"Su expectativa es comprensible pero el compilador no la exige". - Lo sé, pero mi pregunta es cómo escribir mejor mi código para expresar el contrato que tengo en mente, no cuál es el contrato de otro código.
Sebastian Redl