I'm trying to create a type similar to Rust's Result or Haskell's Either and I've got this far:
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;
}
}
Given that both types parameters are restricted to be notnull, why is it complaining (anywhere where there's a type parameter with the nullable ? sign after it) that:
A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.
?
I'm using C# 8 on .NET Core 3 with nullable reference types enabled.
Basically you're asking for something that can't be represented in IL. Nullable value types and nullable reference types are very different beasts, and while they look similar in source code, the IL is very different. The nullable version of a value type T is a different type (Nullable<T>) whereas the nullable version of a reference type T is the same type, with attributes telling the compiler what to expect.
Consider this simpler example:
public class Foo<T> where T : notnull
{
public T? GetNullValue() =>
}
That's invalid for the same reason.
If we constraint T to be a struct, then the IL generated for the GetNullValue method would have a return type of Nullable<T>.
If we constraint T to be a non-nullable reference type, then the IL generated for the GetNullValue method would have a return type of T, but with an attribute for the nullability aspect.
The compiler can't generate IL for a method which has a return type of both T and Nullable<T> at the same time.
This is basically all the result of nullable reference types not being a CLR concept at all - it's just compiler magic to help you express intentions in code and get the compiler to perform some checking at compile-time.
The error message isn't as clear as it might be though. T is known to be "a value type or non-nullable reference type". A more precise (but significantly wordier) error message would be:
A nullable type parameter must be known to be a value type, or known to be a non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.
At that point the error would reasonably apply to our code - the type parameter is not "known to be a value type" and it's not "known to be a non-nullable reference type". It's known to be one of the two, but the compiler needs to know which.
The reason for the warning is explained in the section The issue with T? of Try out Nullable Reference Types. Long story short, if you use T? you have to specify whether the type is a class or struct. You may end up creating two types for each case.
The deeper problem is that using one type to implement Result and hold both Success and Error values brings back the same problems Result was supposed to fix, and a few more.
The same type has to carry a dead value around, either the type or the error, or bring back nulls
Pattern matching on the type isn't possible. You'd have to use some fancy positional pattern matching expressions to get this to work.
To avoid nulls you'll have to use something like Option/Maybe, similar to F#'s Options. You'd still carry a None around though, either for the value or error.
Result (and Either) in F#
The starting point should be F#'s Result type and discriminated unions. After all, this already works on .NET.
A Result type in F# is :
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
The types themselves only carry what they need.
DUs in F# allow exhaustive pattern matching without requiring nulls :
match res2 with
| Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
| Error e -> printfn "Error: %s" e
Emulating this in C# 8
Unfortunately, C# 8 doesn't have DUs yet, they're scheduled for C# 9. In C# 8 we can emulate this, but we lose exhaustive matching :
#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;
}
And use it :
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()
};
}
Without exhaustive pattern matching, we have to add that default clause to avoid compiler warnings.
I'm still looking for a way to get exhaustive matching without introducing dead values, even if they are just an Option.
Option/Maybe
Creating an Option class by the way that uses exhaustive matching is simpler :
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;
}
Which can be used with :
string cateGory = someValue switch { Option<Category> (_ ,false) =>"No Category",
Option<Category> (var v,true) => v.Name
};
Related
I have a generic class that should operate on (non-nullable) reference and value types (parameters, returns ...) but internally needs fields that can be null.
using System;
public class Gen<T> // where T : struct
{
public class Data
{
public T? t;
}
public static void Write(string s)
{
Data d = new Data();
Console.WriteLine("Default of {0} is {1}", s, d.t == null ? "null" : "NOT null");
}
// ... other stuff that uses T and not T? like
// public T DoSomething(T value) ...
}
static class Program
{
static void Main(string[] args)
{
Gen<int>.Write("int?");
Gen<string>.Write("string?");
}
}
This code does not produce any errors or warnings when compiled (.NET 5) with nullable enabled.
However the behavior is not as I have expected.
Default of int? is NOT null
Default of string? is null
While playing around searching for a solution, I discovered that when the where T : struct constraint is added (and Gen.Write() removed), the behavior changes to
Default of int? is null
It's odd that a constraint changes the behavior.
Does anybody know a elegant solution to write such a generic class?
Using a custom Nullable class that supports reference types too or a separate bool flags for every T? filed is a bit tedious.
If you want to use Nullable<int> you shouldn't use int, so use:
Gen<int?>.Write("int?");
Then the output will be
Default of int? is null
Default of string? is null
The code in the question is an example. The real class does not have a
Write method and never uses the string of the type. However as I
indicated by 'other stuff' it uses T as well as T?. So it is not
desired to instantiate it with int? instead of int.
First i want to explain why it's not odd that the struct constraint in the generic class changes the behavior. Because actually that constraint makes it compile if you are < C#8. Then T? means Nullable<T>, so if you use Gen<int>.Write("int?") the field t will be a Nullable<int>. But then Gen<string>.Write("string") won't compile at all since string is not a struct. So it has a completely different meaning with the constraint.
With C#8 enabled you can remove the struct constrained, then t remains an int and the string will be a nullable string. So the question mark has the meaning: in case of a reference type it's a nullable reference type, otherwise it's just what it is.
You can't have both, a generic type that can be a nullable reference type or a nullable value type without using the desired generic type, so use int? if it must be nullable.
public readonly struct Foo<T>
{
// ...
public T Value { get; }
}
public static Foo<string?> Bar(Foo<string?> foo)
{
if (foo.Value is null) { /* Something */ }
else { /* Some other thing */ }
return foo;
}
public static void Main()
{
var foo1 = new Foo<string>("s");
var ret1 = Bar(foo1); // Warns because foo1's type parameter is not nullable
var foo2 = new Foo<string?>("s");
var ret2 = Bar(foo2); // Ok because foo2's type parameter is nullable
}
Can I write Bar to accept both Foo<string?> and Foo<string>?
Can I annotate Bar to specify "what goes in goes out", so the nullability of T of the returning Foo<T> will be the same as the nullability of T of the parameter Foo<T>?
For a Foo<string?> Bar(Foo<string?> foo) method, the type parameter of the accepted foo has no relation to the type parameter of the returned foo. And currently, there is no way to annotate the intended relationship of their nullabilities.
Type parameter T in a generic Foo<T> Bar<T>(Foo<T> foo) method, however, would allow us to share the nullability and we could add a generic type constraint like where T : MyType? in order to use type-specific functionality.
Unfortunately, we can't write where T : string?, because C# does not allow sealed classes to be used as type constraints. This is reasonable as it wouldn`t make sense before - why would we design the method as generic at the first place if there is only one type we can use anyway? Well, with C# 8 and nullable reference types are out in the wild, I think we may have a valid reason now. Maybe the team can lift this restriction as they lifted it for enums.
To answer my own question:
There is no attribute to annotate this use case.
Generics can be used with type constraints but only with types that are not sealed.
As a side note: Even though C# doesn't let us use sealed types as type constraints, IL does. So if weaving is an option, we can actually add sealed type constraints to generic type parameters. A solution like ExtraConstraints can be extended to provide this functionality.
I have a generic interface IDataAdapter<T>; implementors of the interface should be able to read POCOs with Guid IDs from a data source. IDataAdapter<T> has a method Read(Guid id) which I want to return a T?, where null indicates that no matches were found in the data source. However, even with a constraint T : notnull on IDataAdapter<T>, attempting to define this method gives the error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint. Why am I still getting this error, even with T constrained to notnull?
Code (should be in a C# 8 environment with <Nullable>enable</Nullable>):
interface IDataAdapter<T> where T : notnull
{
T? Read (Guid id); // error CS8627
}
I think this issue is very similar to what is happening in this post.
Note that a T? where T : class and a T? where T : struct are represented very differently in the CLR. The former is just the CLR type T. There are not separate types in the CLR to differentiate between T and T?. T? in C# just adds extra compile time checking by the C# compiler. On the other hand, The latter is represented by the CLR type Nullable<T>.
So let's consider your method:
T? Read (Guid id);
How should this be represented in the CLR? What is the return type? The compiler don't know whether T is a reference type or a value type, so the compiler cannot decide whether the method signature should be:
T Read (Guid id);
or:
Nullable<T> Read (Guid id);
The same error is raised if you don't use the notnull constraint. You need to specify what that type is with a class or struct constraint. You don't need to specify notnull as structs were always nullable and, with nullable ref types enabled, so are classes.
Just add where T:class or where T:struct.
Reference Types
If you add the class constraint, eg:
#nullable enable
interface IDataAdapter<T>
where T:class
{
T? Read (Guid id); // error CS8627
void Something(T input);
}
class StringAdapter:IDataAdapter<string>
{
public string Read(Guid id)=>id.ToString();
public void Something(string input){}
}
The following call will generate a warning:
var adp=new StringAdapter();
string? x=null;
adp.Something(x); //CS8604: Possible null reference argument ....
Value Types
Using struct to create an IntAdapter on the other hand results in a compilation error if the argument is nullable :
interface IDataAdapter<T>
where T:struct
{
T? Read (Guid id); // error CS8627
void Something(T input);
}
class IntAdapter:IDataAdapter<int>
{
public int? Read(Guid id)=>id.ToString().Length;
public void Something(int input){}
}
void Main()
{
var adp=new IntAdapter();
int? x=null;
adp.Something(x); //CS1503: Cannot convert from int? to int
}
That's because the compile generated methods that expect an int? instead of an int.
Explanation
The reason is that the compiler has to generate very different code in each case. For a class, it doesn't have to do anything special. For a struct, it has to generate a Nullable< T>.
This is explained in the The issue with T? section in Try out Nullable Reference Types :
This distinction between nullable value types and nullable reference types comes up in a pattern such as this:
void M<T>(T? t) where T: notnull
This would mean that the parameter is the nullable version of T, and T is constrained to be notnull. If T were a string, then the actual signature of M would be M([NullableAttribute] T t), but if T were an int, then M would be M(Nullable t). These two signatures are fundamentally different, and this difference is not reconcilable.
Because of this issue between the concrete representations of nullable reference types and nullable value types, any use of T? must also require you to constrain the T to be either class or struct.
If you look at Nullable Struct's documentation you can see that it requires to be a struct:
public struct Nullable<T> where T : struct
I believe you will need to constraint T to be a struct:
interface IA<T> where T : struct
{
T? Read(Guid id);
// Or Nullable<T> Read(Guid id);
}
class A : IA<int>
{
public int? Read(Guid id) { Console.WriteLine("A"); return 0; }
}
BTW. Could you give us an example of what types you want to use this class with?
Why not just use where T: class and return T (or even not have a constraint at all)?
interface IA<T> where T : class
{
T Read(Guid id);
}
I want to create a generic class that has a member of type T. T may be a class, a nullable class, a struct, or a nullable struct. So basically anything. This is a simplified example that shows my problem:
#nullable enable
class Box<T> {
public T Value { get; }
public Box(T value) {
Value = value;
}
public static Box<T> CreateDefault()
=> new Box<T>(default(T));
}
Due to using the new #nullable enable feature I get the following warning: Program.cs(11,23): warning CS8653: A default expression introduces a null value when 'T' is a non-nullable reference type.
This warning makes sense to me. I then tried to fix it by adding a ? to the property and constructor parameter:
#nullable enable
class Box<T> {
public T? Value { get; }
public Box(T? value) {
Value = value;
}
public static Box<T> CreateDefault()
=> new Box<T>(default(T));
}
But now I get two errors instead:
Program.cs(4,12): error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.
Program.cs(6,16): error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.
However, I don't want to add a constraint. I don't care if T is a class or a struct.
An obvious solution is to wrap the offending members under a #nullable disable directive. However, like #pragma warning disable, I'd like to avoid doing that unless it's necessary. Is there another way in getting my code to compile without disabling the nullability checks or the CS8653 warning?
$ dotnet --info
.NET Core SDK (reflecting any global.json):
Version: 3.0.100-preview4-011223
Commit: 118dd862c8
What to do if you are using C# 9
In C# 9, you can use T? on an unconstrained type parameter to indicate that the type is always nullable when T is a reference type. In fact, the example in the original question "just works" after adding ? to the property and constructor parameter. See the following example to understand what behaviors you may expect for different kinds of type arguments to Box<T>.
var box1 = Box<string>.CreateDefault();
// warning: box1.Value may be null
box1.Value.ToString();
var box2 = Box<string?>.CreateDefault();
// warning: box2.Value may be null
box2.Value.ToString();
var box3 = Box<int>.CreateDefault();
// no warning
box3.Value.ToString();
var box4 = Box<int?>.CreateDefault();
// warning: 'box4.Value' may be null
box4.Value.Value.ToString();
What to do if you are using C# 8
In C# 8, it is not possible to put a nullable annotation on an unconstrained type parameter (i.e. that is not known to be of a reference type or value type).
As discussed in the comments on this question, you will probably need to take some thought as to whether a Box<string> with a default value is valid or not in a nullable context and potentially adjust your API surface accordingly. Perhaps the type has to be Box<string?> in order for an instance containing a default value to be valid. However, there are scenarios where you will want to specify that properties, method returns or parameters, etc. could still be null even though they have non-nullable reference types. If you are in that category, you will probably want to make use of nullability-related attributes.
The MaybeNull and AllowNull attributes have been introduced to .NET Core 3 to handle this scenario.
Some of the specific behaviors of these attributes are still evolving, but the basic idea is:
[MaybeNull] means that the output of something (reading a field or property, a method return, etc.) could be null.
[AllowNull] means that the input to something (writing a field or property, a method parameter, etc.) could be null.
#nullable enable
using System.Diagnostics.CodeAnalysis;
class Box<T>
{
// We use MaybeNull to indicate null could be returned from the property,
// and AllowNull to indicate that null is allowed to be assigned to the property.
[MaybeNull, AllowNull]
public T Value { get; }
// We use only AllowNull here, because the parameter only represents
// an input, unlike the property which has both input and output
public Box([AllowNull] T value)
{
Value = value;
}
public static Box<T> CreateDefault()
{
return new Box<T>(default);
}
public static void UseStringDefault()
{
var box = Box<string>.CreateDefault();
// Since 'box.Value' is a reference type here, [MaybeNull]
// makes us warn on dereference of it.
_ = box.Value.Length;
}
public static void UseIntDefault()
{
// Since 'box.Value' is a value type here, we don't warn on
// dereference even though the original property has [MaybeNull]
var box = Box<int>.CreateDefault();
_ = box.Value.ToString();
}
}
Please see https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types for more information, particularly the section "the issue with T?".
Jeff Mercado raised a good point in the comments:
I think you have some conflicting goals here. You want to have the notion of a default box but for reference types, what else is an appropriate default? The default is null for reference types which directly conflicts with using nullable reference types. Perhaps you will need to constrain T to types that could be default constructed instead (new()).
For example, default(T) for T = string would be null, since at runtime there is no distinction between string and string?. This is a current limitation of the language feature.
I have worked around this limation by creating separate CreateDefault methods for each case:
#nullable enable
class Box<T> {
public T Value { get; }
public Box(T value) {
Value = value;
}
}
static class CreateDefaultBox
{
public static Box<T> ValueTypeNotNull<T>() where T : struct
=> new Box<T>(default);
public static Box<T?> ValueTypeNullable<T>() where T : struct
=> new Box<T?>(null);
public static Box<T> ReferenceTypeNotNull<T>() where T : class, new()
=> new Box<T>(new T());
public static Box<T?> ReferenceTypeNullable<T>() where T : class
=> new Box<T?>(null);
}
This seems type safe to me, at the cost of more ugly call sites (CreateDefaultBox.ReferenceTypeNullable<object>() instead of Box<object?>.CreateDefault()). In the example class I posted I'd just remove the methods completely and use the Box constructor directly. Oh well.
class Box<T> {
public T! Value { get; }
public Box(T! value) {
Value = value;
}
public static Box<T> CreateDefault()
=> new default!;
}
What does null! statement mean?
I'm playing around a bit with the new C# 8 nullable reference types feature, and while refactoring my code I came upon this (simplified) method:
public T Get<T>(string key)
{
var wrapper = cacheService.Get(key);
return wrapper.HasValue ? Deserialize<T>(wrapper) : default;
}
Now, this gives a warning
Possible null reference return
which is logical, since default(T) will give null for all reference types. At first I thought I would change it to the following:
public T? Get<T>(string key)
But this cannot be done. It says I either have to add a generic constraint where T : class or where T : struct. But that is not an option, as it can be both (I can store an int or int? or an instance of FooBar or whatever in the cache).
I also read about a supposed new generic constraint where class? but that did not seem to work.
The only simple solution I can think of is changing the return statement using a null forgiving operator:
return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;
But that feels wrong, since it can definitely be null, so I'm basically lying to the compiler here..
How can I fix this? Am I missing something utterly obvious here?
You were very close. Just write your method like this:
[return: MaybeNull]
public T Get<T>(string key)
{
var wrapper = cacheService.Get(key);
return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;
}
You have to use the default! to get rid of the warning. But you can tell the compiler with [return: MaybeNull] that it should check for null even if it's a non-nullable type.
In that case, the dev may get a warning (depends on flow analytics) if he uses your method and does not check for null.
For further info, see Microsoft documentation: Specify post-conditions: MaybeNull and NotNull
I think default! is the best you can do at this point.
The reason why public T? Get<T>(string key) doesn't work is because nullable reference types are very different from nullable value types.
Nullable reference types is purely a compile time thing. The little question marks and exclamation marks are only used by the compiler to check for possible nulls. To the eyes of the runtime, string? and string are exactly the same.
Nullable value types on the other hand, is syntactic sugar for Nullable<T>. When the compiler compiles your method, it needs to decide the return type of your method. If T is a reference type, your method would have return type T. If T is a value type, your method would have a return type of Nullable<T>. But the compiler don't know how to handle it when T can be both. It certainly can't say "the return type is T if T is a reference type, and it is Nullable<T> if T is a reference type." because the CLR wouldn't understand that. A method is supposed to only have one return type.
In other words, by saying that you want to return T? is like saying you want to return T when T is a reference type, and return Nullable<T> when T is a value type. That doesn't sound like a valid return type for a method, does it?
As a really bad workaround, you could declare two methods with different names - one has T constrained to value types, and the other has T constrained to reference types:
public T? Get<T>(string key) where T : class
{
var wrapper = cacheService.Get(key);
return wrapper.HasValue ? Deserialize<T>(wrapper) : null;
}
public T? GetStruct<T>(string key) where T : struct
{
var wrapper = cacheService.Get(key);
return wrapper.HasValue ? (T?)Deserialize<T>(wrapper) : null;
}
In C# 9 you are able to express nullability of unconstrained generics more naturally:
public T? Get<T>(string key)
{
var wrapper = cacheService.Get(key);
return wrapper.HasValue ? Deserialize<T>(wrapper) : default;
}
Note there's no ! operator on the default expression. The only change from your original example is the addition of ? to the T return type.
In addition to Drew's answer about C# 9
Having T? Get<T>(string key) we still need to distinguish nullable ref types and nullable value types in the calling code:
SomeClass? c = Get<SomeClass?>("key"); // return type is SomeClass?
SomeClass? c2 = Get<SomeClass>("key"); // return type is SomeClass?
int? i = Get<int?>("key"); // return type is int?
int i2 = Get<int>("key"); // return type is int