5

我想知道如何在 C# 中将后期初始化的类字段与可为空的引用类型一起使用。想象一下下面的类:

public class PdfCreator { 

   private PdfDoc doc;

   public void Create(FileInfo outputFile) {
       doc = new PdfWriter(outputFile);
       Start();
   }

   public void Create(MemoryStream stream) {
       doc = new PdfWriter(stream);
       Start();
   }

   private void Start() {
      Method1();
      // ...
      MethodN();
   }

   private void Method1() {
      // Work with doc
   }

   // ...

   private void MethodN() {
      // Work with doc
   }
}

上面的代码非常简化。我真正的班级使用了更多的领域,比如doc还有一些带有一些参数的构造函数。

使用上面的代码,我在构造函数上得到一个编译器警告,doc没有初始化,这是正确的。我可以通过将类型设置为docto来解决这个问题PdfDoc?,但是我必须使用?.或使用!.它的任何地方,这很讨厌。

我也可以将doc每个方法作为参数传递,但请记住,我有一些这样的字段,这违反了我眼中的干净代码原则。

我正在寻找一种方法来告诉编译器,我将doc在使用它之前进行初始化(实际上我这样做了,调用者不可能获得空引用异常!)。我认为 Kotlinlateinit正是为此目的而使用了修饰符。

您将如何在“干净”的 C# 代码中解决这个问题?

4

5 回答 5

8

到目前为止我发现的最佳解决方案是这个:

private PdfDoc doc = null!;

这通过使用 C# 8 中引入的null-forgiving 运算符来删除所有编译器警告。它允许您使用一个值,就好像它不是 null 一样。因此,可以使用它的一种方法是当您需要类似于 Kotlin 的“ lateinit ”的东西时。与 Kotlin 的 lateinit 不同,它实际上会在此处初始化为 null,编译器和运行时都允许这样做。如果您稍后在不期望为 null 的情况下使用此变量,您可能会得到 NullReferenceException,并且编译器不会警告您它可能为 null,因为它会认为它不为 null。Kotlin 的 lateinit 有一个微妙的区别,如果你在初始化之前访问了一个 lateinit 属性,它会抛出一个特殊的异常,清楚地标识正在访问的属性以及它没有被访问的事实

于 2020-03-23T11:36:02.167 回答
2

对于可为空的引用类型,后期初始化可能会很棘手

一种选择是使成员变量可以为空并添加一个函数来包装访问:

private PdfDocument? pdfDocument = null;

private PdfDocument GetDocument()
{
  if(pdfDocument == null) throw new InvalidOperationException("not initialized");

  return pdfDocument;
}

pdfDocument请注意,编译器不会对此方法发出警告,因为它认识到该方法仅在不为 null时才会返回。

有了这个地方,您现在可以将您的方法更改为:

private void Method1() 
{
  var doc = GetDocument();

  // doc is never null
}

现在您的代码更准确地模拟了意图。pdfDocument可以null,即使只是很短的时间,并且您的方法可以访问文档,知道他们永远不会回来null

于 2020-03-23T11:40:51.477 回答
1

听起来您想要的是一种在您的方法中添加可空性前提条件的方法(即,如果我在字段 X、Y 或 Z 可能为空时调用此实例方法,请警告我)。语言在这一点上没有。欢迎您在https://github.com/dotnet/csharplang提出语言功能请求。

根据您的类型初始化的确切工作方式,将有不同的模式可以作为替代方案。听起来您有以下阶段:

  1. 使用一些参数调用构造函数,并将参数保存到字段中。
  2. 调用重载Create()并填充“后期初始化”字段。
  3. Create()调用Start(),它几乎可以做其他所有事情。

在这种情况下,我会考虑将使用后期初始化字段的方法提取到另一种类型:

public class PdfCreator {

    public void Create(FileInfo outputFile) {
        var context = new PdfCreatorContext(new PdfWriter(outputFile));
        context.Start();
    }

    public void Create(MemoryStream stream) {
        var context = new PdfCreatorContext(new PdfWriter(stream));
        context.Start();
    }

    private struct PdfCreatorContext
    {
        private PdfDoc doc;
        internal PdfCreatorContext(PdfDoc doc)
        {
            this.doc = doc;
        }

        internal void Start() {
            Method1();
            // ...
            MethodN();
        }

        internal void Method1() {
            // Work with doc
            doc.ToString();
        }

        // ...

        internal void MethodN() {
            // Work with doc
        }
    }
}

类的使用可能比这更复杂,或者异步和变异等问题使得使用struct. 在这种情况下,您至少可以要求您的方法在编译器允许它们使用可空字段之前检查它们自己的先决条件:

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

public class PdfCreator { 
   PdfDoc? doc;

   [Conditional("DEBUG"), MemberNotNull(nameof(doc))]
   private void AssertInitialized()
   {
      Debug.Assert(doc != null);
      // since the real thing has many nullable fields, we check them all
      // in here, and reference them all in the MemberNotNull attribute.
   }

   private void Method1() {
      AssertInitialized();
      // Work with doc with the assumption it is not-null.
      // In the case that any method is called with an unexpected
      // null field in debug builds, we crash as early as possible.
      doc.ToString();
   }

   private void Method2() {
      // oops! we didn't AssertInitialized, so we get a warning.
      doc.ToString(); 
   }
}

请注意,[MemberNotNull]目前仅在 .NET 5 预览版中可用。在 .NET Core 3 中,您可以编写一个 Debug.Assert 来检查调用站点所需的所有可为空的字段。

   private void Method1() {
      Debug.Assert(doc != null);
      doc.ToString();
   }
于 2020-05-23T17:19:53.333 回答
1

您的代码看起来像是构建器模式,请阅读更多相关信息

    public class PdfBuilder
    {
        private PdfDoc _doc;

        private PdfBuilder(PdfDoc doc)
        {
            _doc = doc;
        }

        public static PdfBuilder Builder(FileInfo outputFile)
        {
            var writer = new PdfWriter(outputFile);
            return new PdfBuilder(writer.ReadPdfDoc());
        }

        public void Build() 
        {
            Stage1();
            StageN();
        }

        private void Stage1() 
        {
            // Work with doc
        }

        // ...

        private void StageN() 
        {
            // Work with doc
        }
    }
于 2020-03-23T12:11:33.323 回答
0

它可能不会直接解决 OP 的问题,但由于搜索“late init”将我带到这里,我将发布。

尽管您可以使用null!其他答案中解释的技术,但如果您不是直接在构造函数中而是通过一些辅助方法初始化非空类成员,则有一种更优雅的方式来声明它。您需要MemberNotNull(nameof(Member))在辅助方法上使用属性。

public class TestClass
{
    private string name;

    public TestClass()
    {
        Initialize();
    }

    [MemberNotNull(nameof(name))]
    private void Initialize()
    {
        name = "Initialized";
    }
}

这样,编译器将不再争论name在退出构造函数后不设置不可为空,因为它知道调用Initialize确保该name字段被初始化为非空值。

于 2022-02-10T07:06:28.843 回答