Linq 扩展方法和错误处理

Linq Extension Methods and Error Handling

我有以下 C# 代码...

    // We're essentially pivoting the data, using LINQ's GroupBy.
    var pivotedOperands = Operands.GroupBy(o => new { outputid = (Guid)o[DETAILS_OUTPUTID], unitid = o[DETAILS_UNITID] })
        .Select(g => new
        {
            PivotKey = g.Key,
            c1 = g.Where(x => (int)x[DETAILS_OV_SEQUENCEID] == 1).Sum(x => double.Parse(x[DETAILS_VALUE].ToString())),
            r1 = g.Where(x => (int)x[DETAILS_OV_SEQUENCEID] == 2).Sum(x => double.Parse(x[DETAILS_VALUE].ToString())),
            a1 = g.Where(x => (int)x[DETAILS_OV_SEQUENCEID] == 3).Sum(x => double.Parse(x[DETAILS_VALUE].ToString()))
        });

它获取 Operands(这是一个 List 对象)中的数据,并使用 GroupBy() 扩展方法对数据执行透视。本质上,c1、r1 和 a1 都是序列 ID 分别为 1、2 和 3 的不同 DataRow 对象中的值。 (如果有必要,我可以对此进行更多的保留,但我认为不会。)

所以有时 c1 的值可能为空。 (这不应该,但错误不时会在流程的更上游发生。)如果 c1 不是数值,则 double.Parse() 调用将引发异常。没关系。这是我的问题。例如,如果 Operands 对象包含 9 行,这些行将被转换为 3 行,并且这九个值中的一个不是数字,是否可以确定哪个 DataRow 对象引发了异常?

示例: 如果 Operands 包含以下 SequenceID 和 Value 值...

OutputID UnitID SequenceID Value
A        1      1          '0'
A        1      2          '0'
A        1      3          '0'
A        2      1          ''
A        2      2          '0'
A        2      3          '0'
B        1      1          '0'
B        1      2          '0'
B        1      3          '0'

...然后当它尝试通过 double.Parse() 方法处理我数据集第 4 行的空字符串时,我们将得到一个 "Input string was not in a correct format" 异常。我想向用户提出一个友好的例外,告诉他们哪一行是问题所在;不仅仅是这组数据中某处存在问题。是否可以准确确定导致异常的原因?

如果您在 Visual studio 中创建一个新的 C# 控制台应用程序并将以下代码转储到 Main 方法中,您将能够重现我的问题。

        // Create a DataTable so that we can easily create new DataRows to add to our List.
        DataTable dt = new DataTable();
        DataColumn col = new DataColumn();
        col.DataType = System.Type.GetType("System.String");
        col.ColumnName = "OutputID";
        dt.Columns.Add(col);

        col = new DataColumn();
        col.DataType = System.Type.GetType("System.Int32");
        col.ColumnName = "UnitID";
        dt.Columns.Add(col);

        col = new DataColumn();
        col.DataType = System.Type.GetType("System.Int32");
        col.ColumnName = "SequenceID";
        dt.Columns.Add(col);

        col = new DataColumn();
        col.DataType = System.Type.GetType("System.String");
        col.ColumnName = "Value";
        dt.Columns.Add(col);



        // Create the List and add our sample data
        List<DataRow> Operands = new List<DataRow>();

        DataRow dr = dt.NewRow();
        dr["OutputID"] = "A";
        dr["UnitID"] = "1";
        dr["SequenceID"] = 1;
        dr["Value"] = "0";
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "A";
        dr["UnitID"] = "1";
        dr["SequenceID"] = 2;
        dr["Value"] = "0";
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "A";
        dr["UnitID"] = "1";
        dr["SequenceID"] = 3;
        dr["Value"] = "0";
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "A";
        dr["UnitID"] = "2";
        dr["SequenceID"] = 1;
        dr["Value"] = "";       // This should cause an error.
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "A";
        dr["UnitID"] = "2";
        dr["SequenceID"] = 2;
        dr["Value"] = "0";
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "A";
        dr["UnitID"] = "2";
        dr["SequenceID"] = 3;
        dr["Value"] = "0";
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "B";
        dr["UnitID"] = "1";
        dr["SequenceID"] = 1;
        dr["Value"] = "0";
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "B";
        dr["UnitID"] = "1";
        dr["SequenceID"] = 2;
        dr["Value"] = "0";
        Operands.Add(dr);

        dr = dt.NewRow();
        dr["OutputID"] = "B";
        dr["UnitID"] = "1";
        dr["SequenceID"] = 3;
        dr["Value"] = "0";
        Operands.Add(dr);

        // Now pivot the data
        try
        {
            var pivotedOperands = Operands.GroupBy(o => new { outputid = o[0], unitid = o[1] })
                .Select(g => new
                {
                    PivotKey = g.Key,
                    c1 = g.Where(x => (int)x[2] == 1).Sum(x => double.Parse(x[3].ToString())),
                    r1 = g.Where(x => (int)x[2] == 2).Sum(x => double.Parse(x[3].ToString())),
                    a1 = g.Where(x => (int)x[2] == 3).Sum(x => double.Parse(x[3].ToString()))
                });

            foreach (var o in pivotedOperands)
            {
                Console.WriteLine(string.Format("c1 = {0}; r1 = {1}; a1 = {2}", o.c1, o.r1, o.a1));
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        Console.WriteLine("Done.");
        Console.ReadLine();

根据您希望信息显示的方式,您可以更改结果类型以说明失败的可能性,或者您可以捕获有关异常的上下文信息并抛出包含更多信息的新异常.

无论哪种情况,都不要害怕使用辅助方法。例如,假设您通过创建如下方法摆脱了选择器中的重复代码:

string GetSumOrErrorMessage(int idToMatch, IEnumerable<DataRow> dataRow)
{
    try
    {
        var sum = dataRow.Where(x => (int)x[2] == idToMatch).Sum(x => double.Parse(x[3].ToString()));
        return sum.ToString();
    }
    catch (Exception)
    {
        return "Error happened here"; // or something more specific
    }
}

现在您可以像这样更改您的查询:

    var pivotedOperands = Operands.GroupBy(o => new { outputid = o[0], unitid = o[1] })
        .Select(g => new
        {
            PivotKey = g.Key,
            c1 = GetSumOrErrorMessage(1, g),
            r1 = GetSumOrErrorMessage(2, g),
            a1 = GetSumOrErrorMessage(3, g)
        });

你的输出变成:

c1 = 0; r1 = 0; a1 = 0
c1 = Error happened here; r1 = 0; a1 = 0
c1 = 0; r1 = 0; a1 = 0

如果您喜欢这种模式,而不仅仅是 returning a string,您可能需要研究可以帮助解决这个问题的专门的 Monadic 类型。例如,您可以创建一个 class,它在操作成功时具有通用值,在操作失败时具有错误消息。您可以创建各种扩展方法和帮助程序来使它更容易处理,类似于 my CallMeMaybe library 允许您尝试解析一个值,但只是 return 一个空的 Maybe<>如果解析失败。 (例如 Maybe.From(x[3].ToString()).ParseInt64().Select(i => i.ToString()).Else("Error happened here"))。

或者,如果您确实想在收到错误输入时停止,但仍想知道错误输入在哪里,您可以捕获并抛出:

double GetSum(int idToMatch, IGrouping<object, DataRow> dataRows)
{
    try
    {
        return dataRows.Where(x => (int)x[2] == idToMatch).Sum(x => double.Parse(x[3].ToString()));
    }
    catch (Exception e)
    {
        throw new Exception($"Failure when matching {idToMatch} with group {dataRows.Key}", e);
    }
}

...

    var pivotedOperands = Operands.GroupBy(o => new { outputid = o[0], unitid = o[1] })
        .Select(g => new
        {
            PivotKey = g.Key,
            c1 = GetSum(1, g),
            r1 = GetSum(2, g),
            a1 = GetSum(3, g)
        });

输出:

c1 = 0; r1 = 0; a1 = 0
Failure when matching 1 with group { outputid = A, unitid = 2 }

您尝试使用 TryParse 来绕过异常。如果 TryParse 为假则默认为零 (0)

.Sum(x => { 
    double value = 0; 
    return double.TryParse(x[DETAILS_VALUE].ToString(), out value) ? value : 0; 
})