消除冗余的空值检查:C# 中的方法分解和空值检查

huangapple go评论72阅读模式
英文:

Eliminate redundant null checks: method decomposition and nullability in c#

问题

有关 C# 中的可空性,一切都很出色。但在分解代码时应该如何处理它呢?

想象一下,我们有一个大的方法。它接收某个对象,检查重要字段是否为空,然后进行处理。编译器将属性视为非空直到方法结束:

private class Person
{
    public string? Name { get; set; }
}

// 在分解之前
private void Do(Person person)
{
    if (person.Name is null)
        return;
    Console.WriteLine(person.Name.Contains("John"));
    Console.WriteLine(person.Name.Length);
}

这个方法很大,所以我们必须将它分解成一组小方法:


// 在分解之后
private void Do(Person person)
{
    WriteIfNameIsJohn(person);
    WriteNameLength(person);
}

private void WriteNameLength(Person person)
{
    if (person.Name != null)
        Console.WriteLine(person.Name.Length);
}


private void WriteIfNameIsJohn(Person person)
{
    if (person.Name != null)
        Console.WriteLine(person.Name.Contains("John"));
}

但现在我们应该在每个新方法的开头检查属性是否为空!如果我们创建了三个方法 - 就要检查三次!对于每个“提取方法”操作,都需要添加空值检查。

可能的解决方案是使用某种“已验证”类型并将其传递给新方法:

private class VerifiedPerson : Person
{
    public override string Name { get; set; }
    
    private VerifiedPerson(string name)
    {
        Name = name;
    }

    public static VerifiedPerson? GetVerifiedOrNull(Person person) =>
        person.Name is { } name
            ? new VerifiedPerson(name)
            : null;
}

如何以优雅的方式消除这些检查呢?也许可以以某种方式传递对象属性的空状态?

谢谢。

英文:

Everything about nullability is great in c#. But how should we deal with it when decomposing code?

Imagine that we have one big method. It receives some object, check important fields for null and than processes them. Property is considered by compiler as non-null till the end of method:

        private class Person
        {
            public string? Name { get; set; }
        }
        
        //before
        private void Do(Person person)
        {
            if (person.Name is null)
                return;
            Console.WriteLine(person.Name.Contains("John"));
            Console.WriteLine(person.Name.Length);
        }

This method is big so we must decompose it into set of small ones:

      
        //after
        private void Do(Person person)
        {
            WriteIfNameIsJohn(person);
            WriteNameLength(person);
        }

        private void WriteNameLength(Person person)
        {
            if (person.Name != null)
                Console.WriteLine(person.Name.Length);
        }

        
        private void WriteIfNameIsJohn(Person person)
        {
            if (person.Name != null)
                Console.WriteLine(person.Name.Contains("John"));
        }

But now we should check property for null at the beginning of each new method! If we created three methods - check three times! For each "Exctract method" action it is needed to add null check.

Possible, but verbose and unclean solution is to use some sort of "Verified" type and pass it to new methods:

        private class VerifiedPerson : Person
        {
            public override string Name { get; set; }
            
            private VerifiedPerson(string name)
            {
                Name = name;
            }

            public static VerifiedPerson? GetVerifiedOrNull(Person person) =>
                person.Name is { } name
                    ? new VerifiedPerson(name)
                    : null;
        }

How to eliminate those checks in elegant way? Maybe it is possible to pass somehow null state of object's property?

Thanks)

答案1

得分: 0

一种简单的解决方案是不要在这些方法中检查 null。这将验证留给调用者,但由于所有方法都是私有的,这应该是可以接受的。现在对象仍然会被验证,但只在一个地方。不过,在调用这些辅助方法之前,确保首先验证参数是您的责任:

private void Do(Person person)
{
    if (person?.Name != null)
    {
        WriteIfNameIsJohn(person);
        WriteNameLength(person);
    }
}

private void WriteNameLength(Person person)
{
    Console.WriteLine(person.Name.Length);
}

private void WriteIfNameIsJohn(Person person)
{
    Console.WriteLine(person.Name.Contains("John"));
}

如果验证检查繁琐,可以将其提取到一个辅助方法中:

```csharp
private bool IsValid(Person person)
{
    if (person == null) return false;
    if (person.Name == null) return false;
    // 等等...
    return true;
}

private void Do(Person person)
{
    if (IsValid(person))
    {
        WriteIfNameIsJohn(person);
        WriteNameLength(person);
    }
}
英文:

One simple solution is to not check for null in those methods. This leaves the verification to the caller, but since all the methods are private that should be acceptable. Now the object is still being verified, but only in one place. It's up to you to be sure not to call those helper methods without first validating the arguments, though:

private void Do(Person person)
{
    if (person?.Name != null)
    {
        WriteIfNameIsJohn(person);
        WriteNameLength(person);
    }
}

private void WriteNameLength(Person person)
{
    Console.WriteLine(person.Name.Length);
}

private void WriteIfNameIsJohn(Person person)
{
    Console.WriteLine(person.Name.Contains("John"));
}

And if the validation checks are cumbersome, they could be pulled out into a helper method:

private bool IsValid(Person person)
{
    if (person == null) return false;
    if (person.Name == null) return false;
    // etc...
    return true;
}

private void Do(Person person)
{
    if (IsValid(person))
    {
        WriteIfNameIsJohn(person);
        WriteNameLength(person);
    }
}

答案2

得分: 0

我同意Rufus,没有人强迫你检查null,特别是当所有这些方法都是私有的,因此在你的控制之下。在这些私有方法中,成员永远不会为null。

我们可以使用null-forgiving-operator ! 来帮助编译器,例如:

private void WriteNameLength(Person person)
{
    Console.WriteLine(person.Name!.Length);
}

不过,我会认为当你的逻辑非常复杂时,考虑从方法中提取一个新类,并将名字作为该类的成员可能是值得的。然后,你可以在类的构造函数中仅执行一次检查,然后在所有私有方法中忘记任何null值。

// 提取后
private void Do(Person person)
{
    if(person.Name is null) return;

    var writer = new NameWriter(person.Name);
    writer.WriteIfNameIsJohn();
    writer.WriteNameLength();
}

class NameWriter
{
    private readonly string name; // 这里的name不可为空
    public NameWriter(string name) { this.name = name; }

    public void WriteNameLength()
    {
        // 这里不需要任何null检查
        Console.WriteLine(this.name.Length);
    }
}
英文:

I agree Rufus, no-one forces you to check for null, in particular when all those methods are private and therefor under your control. There's no way the member can ever be null in any of those private methods.

We can help the compiler using the null-forgiving-operator !, e.g:

private void WriteNameLength(Person person)
{
    Console.WriteLine(person.Name!.Length);
}

However I'd argue when your logic is so huge, it may be worth considering to extract a new class from the method and make the name a member of that class. Then you can easily erform the check once within the class` constructor and forget about any nulls in all the private methods.

//after
private void Do(Person person)
{
    if(person.Name is null) return;

    var writer = new NameWriter(person.Name);
    writer.WriteIfNameIsJohn();
    writer.WriteNameLength();
}

class NameWriter
{
    private readonly string name; // name is not nullable here
    public MyClass(string name) { this.name = name }

    public void WriteNameLength()
    {
        // no need for any null-checks here
        Console.WriteLine(this.name.Length);
    }
}

答案3

得分: 0

以下是翻译好的部分:

更改参数

如果方法在某些参数上执行空值检查,您可以直接将这些参数传递给方法,而不是传递初始对象。

示例:在这里,我验证Person.Name,然后将经过验证的名称传递给其他方法。

private void Do(Person person)
{
    if(person.Name is null)
        return;

    WriteIfNameIsJohn(person.Name);
    WriteNameLength(person.Name);
}

private void WriteNameLength(string name)
{
   Console.WriteLine(name.Length);
}

private void WriteIfNameIsJohn(string name)
{
    Console.WriteLine(name.Contains("John"));
}

换句话说,您可以通过将代码拆分成更专业的方法来避免重复检查,这些方法不需要完整的对象(Person),而是仅使用最小的部分(只有Name)。

使用本地函数

如果您绝对需要原始的完整对象,您还可以利用本地函数。这些函数允许提取代码,同时保留原始方法的作用域。

private void Do(Person person)
{
    if(person.Name is null)
        return;

    WriteIfNameIsJohn();
    WriteNameLength();

    void WriteNameLength()
    {
        Console.WriteLine(person.Name.Length);
    }

    void WriteIfNameIsJohn()
    {
        Console.WriteLine(person.Name.Contains("John"));
    }
}
英文:

You can find solutions based on how you define your new methods.

Change the Parameters

If the parent method does the null checks on some parameters, you can directly pass those the child methods instead of passing the initial object.

Example: Here I validate Person.Name and I then pass that validated name to the other methods.

private void Do(Person person)
{
    if(person.Name is null)
        return;

    WriteIfNameIsJohn(person.Name);
    WriteNameLength(person.Name);
}

private void WriteNameLength(string name)
{
   Console.WriteLine(name.Length);
}

private void WriteIfNameIsJohn(string name)
{
    Console.WriteLine(name.Contains("John"));
}

In other words, you can avoid the double-checking by splitting the code into more specialized methods that don't require the full object (Person) but instead work with the minimum (just the Name).

Use Local Functions

If you absolutely need the original full object, you can also leverage local functions. These allow to extract code while keeping the scope of the original method.

private void Do(Person person)
{
    if(person.Name is null)
        return;

    WriteIfNameIsJohn();
    WriteNameLength();

    void WriteNameLength()
    {
        Console.WriteLine(person.Name.Length);
    }

    void WriteIfNameIsJohn()
    {
        Console.WriteLine(person.Name.Contains("John"));
    }
}

huangapple
  • 本文由 发表于 2023年3月4日 05:16:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/75631946.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定