C # não pode tornar o tipo `notull 'nulo

9

Estou tentando criar um tipo semelhante ao de Rust Resultou Haskell Eithere cheguei até aqui:

public struct Result<TResult, TError>
    where TResult : notnull
    where TError : notnull
{
    private readonly OneOf<TResult, TError> Value;
    public Result(TResult result) => Value = result;
    public Result(TError error) => Value = error;

    public static implicit operator Result<TResult, TError>(TResult result)
        => new Result<TResult, TError>(result);

    public static implicit operator Result<TResult, TError>(TError error)
        => new Result<TResult, TError>(error);

    public void Deconstruct(out TResult? result, out TError? error)
    {
        result = (Value.IsT0) ? Value.AsT0 : (TResult?)null;
        error = (Value.IsT1) ? Value.AsT1 : (TError?)null;
    }  
}

Dado que os parâmetros dos dois tipos estão restritos notnull, por que está reclamando (em qualquer lugar onde exista um parâmetro do tipo com o ?sinal nulo ):

Um parâmetro de tipo anulável deve ser conhecido como um tipo de valor ou tipo de referência não anulável. Considere adicionar uma restrição de 'classe', 'estrutura' ou tipo.

?


Estou usando o C # 8 no .NET Core 3 com os tipos de referência anuláveis ​​ativados.

Shoe Diamente
fonte
Você deve começar pelo tipo de resultado do F # e uniões discriminadas. Você pode facilmente obter algo semelhante no C # 8, sem carregar um valor morto, mas não terá uma correspondência exaustiva. Tentando colocar ambos os tipos na mesma estrutura vai correr em um problema após o outro, e traz de volta a própria Resultado problemas era para correção
Panagiotis Kanavos

Respostas:

12

Basicamente, você está pedindo algo que não pode ser representado em IL. Tipos de valor nulo e tipos de referência nulo são bestas muito diferentes e, embora pareçam semelhantes no código-fonte, a IL é muito diferente. A versão anulável de um tipo de valor Té um tipo diferente ( Nullable<T>) enquanto a versão anulável de um tipo de referência Té do mesmo tipo, com atributos informando ao compilador o que esperar.

Considere este exemplo mais simples:

public class Foo<T> where T : notnull
{
    public T? GetNullValue() => 
}

Isso é inválido pelo mesmo motivo.

Se restringirmos Ta uma estrutura, o IL gerado para o GetNullValuemétodo terá um tipo de retorno Nullable<T>.

Se restringirmos Ta ser um tipo de referência não anulável, a IL gerada para o GetNullValuemétodo terá um tipo de retorno deT , mas com um atributo para o aspecto de anulabilidade.

O compilador não pode gerar IL para um método que tenha um tipo de retorno de ambos Te Nullable<T>ao mesmo tempo.

Isso é basicamente o resultado de os tipos de referência anuláveis ​​não serem um conceito CLR - é apenas a mágica do compilador para ajudá-lo a expressar intenções no código e fazer com que o compilador execute algumas verificações no momento da compilação.

A mensagem de erro não é tão clara quanto pode ser. Té conhecido por ser "um tipo de valor ou tipo de referência não anulável". Uma mensagem de erro mais precisa (mas significativamente mais detalhada) seria:

Um parâmetro de tipo anulável deve ser conhecido como um tipo de valor ou um tipo de referência não anulável. Considere adicionar uma restrição de 'classe', 'estrutura' ou tipo.

Nesse ponto, o erro seria razoavelmente aplicado ao nosso código - o parâmetro type não é "conhecido por ser um tipo de valor" e "não é conhecido por ser um tipo de referência não anulável". É conhecido por ser um dos dois, mas o compilador precisa saber qual .

Jon Skeet
fonte
Também há magia de tempo de execução - você não pode tornar um anulável anulável, mesmo que não haja maneira de representar essa restrição na IL. Nullable<T>é um tipo especial que você não pode fazer por si mesmo. E há o ponto de bônus de como o boxe é feito com tipos de nulllable.
Luaan 14/11/19
11
@Luaan: Há mágica em tempo de execução para tipos de valores anuláveis, mas não para tipos de referência anuláveis.
Jon Skeet
6

O motivo do aviso é explicado na seção The issue with T? de experimentar tipos anuláveis referência . Para encurtar a história, se você usar, T?precisará especificar se o tipo é uma classe ou estrutura. Você pode criar dois tipos para cada caso.

O problema mais profundo é que o uso de um tipo para implementar o Result e reter os valores de Sucesso e Erro traz de volta os mesmos problemas que o resultado deveria corrigir e mais alguns.

  • O mesmo tipo precisa carregar um valor morto, o tipo ou o erro, ou trazer de volta nulos
  • A correspondência de padrões no tipo não é possível. Você precisaria usar expressões sofisticadas de correspondência de padrões posicionais para que isso funcionasse.
  • Para evitar nulos, você terá que usar algo como Option / Maybe, semelhante às Opções do F # . Você ainda carregaria um None por aí, seja pelo valor ou pelo erro.

Resultado (e qualquer um) em F #

O ponto de partida deve ser o tipo de resultado do F # e as uniões discriminadas. Afinal, isso já funciona no .NET.

Um tipo de resultado em F # é:

type Result<'T,'TError> =
    | Ok of ResultValue:'T
    | Error of ErrorValue:'TError

Os próprios tipos carregam apenas o que precisam.

As DUs em F # permitem uma correspondência exaustiva de padrões sem exigir nulos:

match res2 with
| Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
| Error e -> printfn "Error: %s" e

Emulando isso em C # 8

Infelizmente, o C # 8 ainda não tem DUs, eles estão agendados para o C # 9. No C # 8 podemos imitar isso, mas perdemos uma correspondência exaustiva:

#nullable enable

public interface IResult<TResult,TError>{}​

struct Success<TResult,TError> : IResult<TResult,TError>
{
    public TResult Value {get;}

    public Success(TResult value)=>Value=value;

    public void Deconstruct(out TResult value)=>value=Value;        
}

struct Error<TResult,TError> : IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error)=>ErrorValue=error;

    public void Deconstruct(out TError error)=>error=ErrorValue;
}

E use-o:

IResult<double,string> Sqrt(IResult<double,string> input)
{
    return input switch {
        Error<double,string> e => e,
        Success<double,string> (var v) when v<0 => new Error<double,string>("Negative"),
        Success<double,string> (var v)  => new Success<double,string>(Math.Sqrt(v)),
        _ => throw new ArgumentException()
    };
}

Sem uma correspondência exaustiva de padrões, precisamos adicionar essa cláusula padrão para evitar avisos do compilador.

Ainda estou procurando uma maneira de obter uma correspondência exaustiva sem introduzir valores mortos, mesmo que sejam apenas uma opção.

Opção / Talvez

Criar uma classe Option pela maneira que usa uma correspondência exaustiva é mais simples:

readonly struct Option<T> 
{
    public readonly T Value {get;}

    public readonly bool IsSome {get;}
    public readonly bool IsNone =>!IsSome;

    public Option(T value)=>(Value,IsSome)=(value,true);    

    public void Deconstruct(out T value,out bool isSome)=>(value,isSome)=(Value,IsSome);
}

//Convenience methods, similar to F#'s Option module
static class Option
{
    public static Option<T> Some<T>(T value)=>new Option<T>(value);    
    public static Option<T> None<T>()=>default;
}

Que pode ser usado com:

string cateGory = someValue switch { Option<Category> (_    ,false) =>"No Category",
                                     Option<Category> (var v,true)  => v.Name
                                   };
Panagiotis Kanavos
fonte