如何告诉C#一个结构体的非空字段在可空性分析目的下可能为空?

3
这是一个启用了可空引用类型的示例C#程序:
using System;
using System.Collections.Generic;
using System.Linq;

MyStruct myStruct = new("A");
List<MyStruct> list = new() { myStruct };
MyStruct found = list.FirstOrDefault(item => item.Str == "B");
Console.WriteLine(found.Str.Length);

struct MyStruct
{
    public readonly string Str;

    public MyStruct(string str)
    {
        Str = str;
    }
}

请注意,MyStruct 包含一个非空的 Str 字段。从理论上讲,这意味着 Str 字段永远不应该为空,并且编译器在几乎所有情况下都会警告您是否将其留空。
然而,如果通过通用方法返回未初始化的结构体(例如使用上面的 FirstOrDefault 调用),则可以滑入 null 值。在这种情况下,Str 字段将为空,但 C# 编译器不会发出任何警告,无论是在访问 Str 还是在分配 found 变量时,因此当程序尝试访问 found.Str.Length 时,程序会崩溃并显示 NullReferenceException。(另一个情况是从数组中读取结构体。)
更糟糕的是,一些代码分析工具会错误地警告不要检查found.Str是否为空。(例如,如果我添加if(found.Str != null),Resharper将报告为“表达式始终为真”,并提示删除它,即使在这种情况下它绝对不是真的。)
这似乎是C#中可空性分析的一个重大“漏洞”,我必须想知道是否有什么方法可以让编译器理解这种情况。有没有办法“告诉”编译器found结构体的字段可能为空,即使它们被声明为不可为空?

编辑:澄清一下,我知道this articlethis question的答案,它们解释了为什么会发生这种情况。但我感兴趣的是该怎么办。具体来说,有没有办法告诉编译器某个实例字段可能为空,即使它被标记为非空,而不必更改该字段的实际声明以使其可为空。就像你可以在表达式后面添加!来告诉编译器,“相信我,这不是null,尽管它被标记为可为空”,我想做的是相反的,即“相信我,尽管它被标记为非空,但它可能为null”。(如果有一种方法可以自动处理结构体实例的所有字段,那就更好了,但我对此表示怀疑。)


2
这可能也是您所看到的内容:https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references#known-pitfalls - Fredou
1
根据文档中“已知陷阱”的部分,似乎这是一个已知的问题。不过我还是很好奇是否有人有应对策略。 - Walt D
1
Nullable 已启用。 - Walt D
1
@JesseGood 我相信那个答案涉及到同样的根本问题,尽管我会认为我的问题不是“为什么”会发生,而是“该怎么办”。但我开始怀疑,对于我的问题,答案可能是“你无法对此做任何事情”。 - Walt D
显示剩余10条评论
1个回答

3
逆转!(空值忽略)运算符的是[MaybeNull]属性。

要使此属性在.netstandard2.0中工作,您可以将NullabilityAttributes.cs的副本放入项目中:

#if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER && !NET5_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis{
   // copy of `NullableAttributes.cs` goes here:
   // https://source.dot.net/#System.Private.CoreLib/NullableAttributes.cs,68093cc4b5713519
}
#endif

使用 .NET 6 NullabilityInfo API 可以看出,[MaybeNull] 属性将 ReadState 设置为 Nullable,而 WriteState 保持为 NotNull
例如:
public readonly struct With_QuestionMark {
    public readonly string? Value;
    public readonly int     Length;

    public With_QuestionMark(object? obj) {
        Value  = obj?.ToString();   // ❌ no warning, because it's OK to assign `null` to `string? Value` 
        Length = Value.Length;      // ✅ warning, because `Value` might be `null`
    }
}

public readonly struct With_MaybeNull {
    [MaybeNull]
    public readonly string Value;
    public readonly int Length;

    public With_MaybeNull(object? obj) {
        Value  = obj?.ToString();   // ✅ warning, because we should never _intentionally_ set `string Value` to `null`
        Length = Value.Length;      // ✅ warning, because `Value` might be `null`
    }
}

我相信这展示了您的KeyValuePair场景:
public static class MaybeNullExample {
    public static void NullableKey() {
        var dic_nullable = new Dictionary<string?, int>();    // ❌ this gives a warning, because `TKey` has a `notnull` constraint
        var key_nullable = dic_nullable.FirstOrDefault().Key; // ✅ implicitly typed to `string?`
        Console.WriteLine(key_nullable.Length);               // ✅ gives the warning we expect
    }

    public static void NonNullKey() {
        var dic_nonnull = new Dictionary<string, int>();    // ✅ no warning, because we satisfy the `notnull` constraint
        var key_nonnull = dic_nonnull.FirstOrDefault().Key; // ❌ implicitly typed to `string`, even though the value is `null`
        Console.WriteLine(key_nonnull.Length);              // ❌ no warning, even though the value is `null`
    }

    public static void MaybeNullKey() {
        // Unfortunately we can't apply attributes to local variables, but we _can_ apply them to local functions
        [return: MaybeNull]
        static string GetKey(KeyValuePair<string, int> kvp) {
            return kvp.Key;
        }

        var dic_maybe = new Dictionary<string, int>();      // ✅ no warning; `notnull` constraint is satisfied
        var key_maybe = GetKey(dic_maybe.FirstOrDefault()); // ✅ implicitly typed to `string?` because of the `[MaybeNull]` attribute
        Console.WriteLine(key_maybe.Length);                // ✅ gives the warning we expect
    }

    ///<remarks>
    /// You can make a generic extension method if you find this occurs often. 
    /// This will preserve the type of `TIn` while still letting the compiler
    /// treat it as nullable.
    ///</remarks>
    [return: MaybeNull]
    static TOut GetMaybeNull<TIn, TOut>(this TIn self, Func<TIn, TOut> extractor) {
        return extractor(self);
    }

    public static void MaybeNullExtension() {
        var dic = new Dictionary<string, int>();                   // ✅ `notnull` constraint is satisfied
        var key = dic.FirstOrDefault().GetMaybeNull(it => it.Key); // ✅ implicitly typed to `string?`
        Console.WriteLine(key.Length);                             // ✅ gives the expected warning
    }
}


谢谢,但正如我在原问题中解释的那样,我正在寻找一种不需要修改结构本身的解决方案,因为我需要能够将其与内置结构(例如KeyValuePair)一起使用。 - Walt D
[MaybeNull],就像!运算符和所有其他的空值属性一样,在几乎任何类型使用的上下文中都可以使用。因此,每当您与结构体交互并且希望安全时,都可以使用[MaybeNull]进行注释。我将添加一个示例以说明我的意思。 - BrandonCimino
嗯,从技术上讲,这解决了问题,但是本地函数有点麻烦。但它给了我一个想法:扩展方法应该也可以工作,而且那会少得多的麻烦。谢谢! - Walt D

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接