如何初始化不可为null的泛型类型属性(或字段)(c#8或9)

如何初始化不可为null的泛型类型属性(或字段)(c#8或9),c#,generics,c#-8.0,nullable-reference-types,c#-9.0,C#,Generics,C# 8.0,Nullable Reference Types,C# 9.0,我正在将一些旧代码转换为c#8和c#9,并启用了nullable。我在泛型方面遇到了一些麻烦。这里有一个例子。较旧的类有一个不受约束的类型参数: public class WeightedValue1<T> { public double Weight { get; set; } public T Value { get; set; } } 公共类加权值1 { 公共双权重{get;set;} 公共T值{get;set;} } 这使得CS8618:退出构造函数时,不

我正在将一些旧代码转换为c#8和c#9,并启用了nullable。我在泛型方面遇到了一些麻烦。这里有一个例子。较旧的类有一个不受约束的类型参数:

public class WeightedValue1<T>
{
    public double Weight { get; set; }
    public T Value { get; set; }
}
公共类加权值1
{
公共双权重{get;set;}
公共T值{get;set;}
}
这使得CS8618:退出构造函数时,不可为null的属性“Value”必须包含非null值

那太好了;它正是为了防止该功能设计用来捕获用户取消引用.Value并获取NullReferenceException的那种错误。问题是很难正确初始化属性。在旧世界,这将是:

public class WeightedValue2<T>
{
    public double Weight { get; set; }
    public T Value { get; set; } = default;
}
公共类加权值2
{
公共双权重{get;set;}
公共T值{get;set;}=default;
}
这就为CS8601提供了可能的空引用赋值。也应该如此!我还没有真正解决这个问题。 即使T是不可为空的字符串,调用方也会在无警告的情况下获得NRE:

[TestClass]
公共类NreTest2
{
[测试方法]
public void非NullableString_ThrowsNre()
{
Assert.ThrowsException(()=>newweightedvalue2().Value.ToLower());
}
}
为了完整起见,请注意指定notnull约束不会改变任何内容

public class WeightedValue3<T>
    where T : notnull
{
    public double Weight { get; set; }
    public T Value { get; set; } = default;
}

[TestClass]
public class NreTest3
{
    [TestMethod]
    public void ConstrainedString_ThrowsNre()
    {
        Assert.ThrowsException<NullReferenceException>(() => new WeightedValue3<string>().Value.ToLower());
    }
}
公共类加权值3
其中T:notnull
{
公共双权重{get;set;}
公共T值{get;set;}=default;
}
[测试类]
公共类NreTest3
{
[测试方法]
public void约束字符串\u ThrowsNre()
{
Assert.ThrowsException(()=>newweightedvalue3().Value.ToLower());
}
}
当然,使用空原谅运算符(!)会隐藏警告,但对调用方毫无帮助

public class WeightedValue4<T>
    where T : notnull
{
    public double Weight { get; set; }
    public T Value { get; set; } = default!;
}
[TestClass]
public class NreTest4
{
    [TestMethod]
    public void ForgivenString_ThrowsNre()
    {
        Assert.ThrowsException<NullReferenceException>(()=> new WeightedValue4<string>().Value.ToLower());
    } 
}
公共类加权值4
其中T:notnull
{
公共双权重{get;set;}
公共T值{get;set;}=default!;
}
[测试类]
公共类NreTest4
{
[测试方法]
公共无效豁免字符串
{
Assert.ThrowsException(()=>newweightedvalue4().Value.ToLower());
} 
}
对,所以问题是字符串的默认值是null,不管我们是否希望它被允许。我们告诉编译器的任何事情都不会让它改变这一点。所以我们必须用一些东西来初始化它。因为它是一个泛型的无约束T,所以我们找不到任何有效值。我们得把这件事推给打电话的人。我现有的所有调用程序都将使用对象初始值设定项语法,因此新的c#9“init”属性似乎非常适合

public class WeightedValue5<T>
{
    public double Weight { get; set; }
    public T Value { get; init; }
}
公共类权重值5
{
公共双权重{get;set;}
公共T值{get;init;}
}
唉。不幸的是,init不能满足我的要求。这意味着您不能在对象创建后调用setter,但它不会强制您实际初始化属性。这让人倍感沮丧,因为编译器知道它需要初始化

[TestClass]
public class NreTest5
{
    [TestMethod]
    public void InitOnlySetter_Compiles_AndThrowsNre()
    {
        Assert.ThrowsException<NullReferenceException>(() => new WeightedValue5<string>().Value.ToLower());
    }
}
[TestClass]
公共类NreTest5
{
[测试方法]
public void InitOnlySetter_编译_和rowsnre()
{
Assert.ThrowsException(()=>newweightedvalue5().Value.ToLower());
}
}
这里有一个版本,它至少可以警告呼叫者可为空。但它需要改变每一个现有的呼叫站点。当你开始有很多属性和多个构造函数需要初始化时,它也有一些常见的麻烦

public class WeightedValue6<T>
{
    public double Weight { get; set; }
    public T Value { get; }

    public WeightedValue6(T value)
    {
        Value = value;
    }
}
公共类加权值6
{
公共双权重{get;set;}
公共T值{get;}
公共加权价值6(T值)
{
价值=价值;
}
}
我还研究了属性注释,但不知道它们有什么帮助。尽管新记录类型的目标是减少简单引用类型的样板文件,但它们也有同样的问题

我疯了吗?我可以用非空值初始化泛型字段的唯一方法是让每个调用方手动指定它们,这是真的吗?唯一的办法就是使用全脂构造器?(当然,还要将特定警告设置为错误。)


如果是这样,希望编译器用init重写我的版本是错误的吗;好像它有完整的构造函数?

您提到记录无法解决问题,但我认为它们在很大程度上减少了这方面的模板

公共记录加权值(双倍加权,T值);
如果将null传递给value,编译器将抱怨启用了可为null的引用类型。还有
with
语法,它确实给了您一点
init
的感觉,但您需要有一个从中复制的值

var-weightedValue=new-weightedValue(0,“你好”);//好的
weightedValue=新的weightedValue(0,null);//编译器警告
weightedValue=weightedValue,带{Value=null};//编译器警告
我也喜欢对象初始化,但是命名参数对我来说非常好。它们唯一不起作用的地方是表达式树(这很烦人)

var-weightedValue=新的weightedValue(
重量:0,,
值:“你好”
);
使用
with
语法的另一个潜在选项是创建一个默认值来初始化类型,然后调用它并更改您对对象感兴趣的参数

public static WeightedValue<string> defaultWeightedString = new(0, "");

//...

var weightedValue = defaultWeightedString with { Weight = 10 }; // string Value initialized
publicstaticweightedvalue defaultWeightedString=new(0,”);
//...
var-weightedValue=defaultWeightedString,带{Weight=10};//字符串值已初始化
我看到这里有很多潜力。然而,如果我们有某种
{get;init required;}
来强制初始化实例化,那就太好了

更新似乎对此有建议


我不明白你在问什么。您希望属性是否可为null。从上面看来,您希望它不可为null。但是在那种情况下
public static WeightedValue<string> defaultWeightedString = new(0, "");

//...

var weightedValue = defaultWeightedString with { Weight = 10 }; // string Value initialized