LINQ,如何在模型中处理空值

4
我正在尝试将AverageRating传递给我的视图。AverageRating是查询Review Model的Icollection中项目的结果。Review Model具有Rating属性。但是我收到以下消息:
System.ArgumentNullException
每当执行get操作时,就会出现这种情况,这是可以理解的。但是,当我的代码如下所示时,我应该如何最好地处理模型或其他地方的空异常:
public class MyModel
{

        //Querying this navigation property
        public ICollection<Review> Reviews { get; set; }

        public double? AverageRating
        {
        get
        { 
            //check that this is not null / handle null
            return Math.Round(Reviews.Average(c => c.Rating), 1);
        }

        }

}

public class Review
{
    [Key]
    public int ReviewID { get; set; }
    public int Rating { get; set; }
    public string Comment { get; set; }
    public int CoachID { get; set; }
    public int? StudentID { get; set; }


    public Coach Coach { get; set; }
    public Student Student { get; set; }
}

@mjwills,Review.Rating 是一个整数。 - thelastchief
@ZdeněkJelínek Jelínek 我在思考,如果一个Review(评论)集合项目为空,则意味着它尚未评级。 - thelastchief
@Sefe 很好的发现!谢谢。 - Zdeněk Jelínek
@Sefe,为什么Reviews不能是null?它为什么会是NRE?文档表明它应该是ANE(因为它是扩展方法)。https://msdn.microsoft.com/en-us/library/bb338413(v=vs.110).aspx - mjwills
1
我建议使用 null,因为它更能反映出“我不知道”的意思,而不是“非常低的分数”。 - mjwills
显示剩余3条评论
4个回答

3

什么是null?

首先,假设null表示未知。Eric Lippert的Null Is Not Empty提供了一个很好的解释。这可以进一步追溯到SQL的设计和三态逻辑原则。一个null集合不是空集,同样,nullint?也不是0。

但即使您不同意,处理nulls有两种基本哲学:

1. 预防null

简单地调整模型,使得在对象的生命周期中始终预防null。这并不总是通过类型系统实现的(特别是在使用.NET序列化时)。这样做可能会导致很多额外的样板代码,所以要明智地使用:

public class Model
{
    // is non-null in any Model instance
    public IReadOnlyList<ModelItem> Items { get; }

    public Model(IEnumerable<ModelItem> items)
    {
        Items = new List<ModelItems>(items); // does not check if items contains null
    }
}

2. 传播和处理 null

当你已经有了一个 null 值时,最好不要遮盖它(这会妨碍维护)。你可以选择将 null 抛出或返回到调用栈上的其他位置,然后强制处理 null 或者继续抛出异常。

public class ModelItem
{
    public double? Value { get; set; }
}

public class Model
{
    public ICollection<ModelItem> Items { get; set; } // for some reason, e.g. serialization, the Items collection can be null

    public double? Average
    {
        get
        {
            if (Items == null)
            {
                // I don't know what items exist => the average is unknown
                return null;
            }

            return Items.Average(i => i?.Value); // note the ?. here to prevent NullReferenceException
       }
    }
}

请注意,Average<Nullable<double>>与非可空变体不同,在空序列时不会引发InvalidOperationException异常,对于非可空类型应添加额外的检查。
另请注意,代码没有试图将null解析为除其他null之外的任何内容。如果您的null在某个地方得到处理,那么它很可能是您的应用程序业务逻辑的一部分,并且应该驻留在相应的层中(例如处理向后兼容性的代码,该代码返回先前版本的模型没有某个属性的情况下,将其作为null返回)。
然而,如果您的模型类本质上假定null集合是一个空集合(出于可读性和维护原因,我强烈建议不这样做),则null确实不应该传播,并且应该在该类内部处理,例如使用合并运算符(??)。

3

这个实现可以满足您的需求:

public double? AverageRating
{
    get
    {
        return Reviews?.Average(x => x?.Rating);
    }
}

它将处理Reviewsnull的情况(它将返回null),因为在Reviews之后使用了?

它将处理单个Reviewsnull的情况(它们将被忽略计算平均值),因为在x之后使用了?


这个可行!我现在能够在我的视图中看到平均评分,它是空的,这正是我想要的。我打算使用三元运算符检查它是否为空,然后只需要一个文本,如“尚未评级”。感谢您的努力,真的非常感激! - thelastchief

1
您可以使用DefaultIfEmpty来为空集设置0值,并从Average计算中排除可能的null值,您应该将它们消除;
    public double? AverageRating
    {
        get
        {
            if (Reviews == null)
            {
                return null;
            }
            return Math.Round(Reviews.Where(x => x.Rating.HasValue).Select(x => x.Rating).DefaultIfEmpty(0).Average().Value, 1);
        }
    }

1
如果你正在使用C# 6.0,你可以使用空值传播来帮助指定默认情况。
代码将会像下面这样:
return Math.Round(Reviews?.Average(c => c.Rating) ?? 0.0, 1);

这里利用了空值传播来确保在访问 Average 扩展方法之前 Reviews 集合不为 Null。
如果有单个项是 NULL,那么你也可以在 lambda 中扩展检查,如下所示:
return Math.Round(Reviews?.Average(c => c?.Rating ?? 0.0) ?? 0.0, 1);

这将防止评论为空或评论项为空。
以下是演示示例:https://dotnetfiddle.net/qBTEyf 如果您需要跳过将NULL翻译为0,则可以先使用Where语句从集合中删除NULL项。
return Math.Round(Reviews?.Where(c => c?.Rating != null).Average(c => c.Rating) ?? 0.0, 1);

这种方法在处理成“平均值”之前会从列表中删除任何空项。 编辑 根据下面的评论,您可以使用DefaultIfEmpty来处理序列本身为空的情况,如下所示:
return Math.Round(Reviews?.DefaultIfEmpty().Average(c => c?.Rating ?? 0.0) ?? 0.0, 1);

调用 DefaultIfEmpty 将返回一个包含一个 null 元素的 IEnumerable。这将在 Average 过程中被过滤掉并返回 0。
这也可以与本文中的其他方法结合使用。fiddle 已更新,添加了使用 DefaultIfEmpty 的测试示例。

使用 return Math.Round(Reviews?.Average(c => c?.Rating ?? 0.0) ?? 0.0, 1); 我得到了 InvalidOperationException: The sequence contains no elements。我的视图:@Html.DisplayFor(modelItem => item.AverageRating)。这个问题可能有什么解决方案?由于我对这些都很陌生,所以有点迷茫... - thelastchief
我的错误。Average 需要一个至少有一个元素的序列才能正确处理。解决方案是在调用 Average 之前检查元素或使用 DefaultIfEmpty - Scott Baldric
1
需要至少一个元素的序列才能正确处理。请注意,这并不适用于所有序列(例如可空整数序列)。 - mjwills

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