使用Lambda表达式避免使用“魔法字符串”来指定属性

6
我正在编写一个服务,将特定类型的对象集合转换为字符串,并输出其原始、字符串和日期时间类型,格式为CSV格式。我已经使以下两个语句都能正常工作。我发现基于lambda的版本更加简洁。

魔法字符串版本

string csv = new ToCsvService<DateTime>(objs)
    .Exclude("Minute")
    .ChangeName("Millisecond", "Milli")
    .Format("Date", "d")
    .ToCsv();

vs. Lambda 版本

string csv = new ToCsvService<DateTime>(objs)
    .Exclude(p => p.Minute)
    .ChangeName(p => p.Millisecond, "Milli")
    .Format(p => p.Date, "d")
    .ToCsv();

根据Jon Skeet的建议,所有lambda方法共享相似的方法签名

public IToCsvService<T> Exclude<TResult>(
        Expression<Func<T, TResult>> expression)

我随后将 expression.Body 传递给 FindMemberExpression。我改编了 ExpressionProcessor.cs 中的 FindMemberExpression 方法的代码,该方法来自于 nhlambdaextensions项目。下面是我非常相似的版本的 FindMemberExpression
private string FindMemberExpression(Expression expression)
{
    if (expression is MemberExpression)
    {
        MemberExpression memberExpression = (MemberExpression)expression;

        if (memberExpression.Expression.NodeType == ExpressionType.MemberAccess
            || memberExpression.Expression.NodeType == ExpressionType.Call)
        {
            if (memberExpression.Member.DeclaringType.IsGenericType
                && memberExpression.Member.DeclaringType
                .GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
            {
                if ("Value".Equals(memberExpression.Member.Name))
                {
                    return FindMemberExpression(memberExpression.Expression);
                }

                return String.Format("{0}.{1}",
                    FindMemberExpression(memberExpression.Expression),
                    memberExpression.Member.Name);
            }
        }
        else
        {
            return memberExpression.Member.Name;
        }
    }

    throw new Exception("Could not determine member from "
        + expression.ToString());
}

我正在测试FindMemberExpression中的足够案例吗?考虑到我的使用情况,我所做的是否过度了?

2个回答

7

编辑:让这个过程更简单的核心是改变你的方法签名,使结果类型也是通用的:

public IToCsvService<TSource> Exclude<TResult>(
    Expression<Func<TSource, TResult>> expression)

这样您就不会得到一个转换表达式,因为不需要进行转换。例如,p => p.Minute由于类型推断会自动变成Expression<Func<DateTime, int>>


对我来说,这似乎有点过度了,因为目前你只需要一个属性 - 至少,这是你的示例显示的所有内容。

为什么不先识别一个属性,如果需要的话再扩展呢?

编辑:以下是一个简短但完整的示例,其中不显示任何转换:

using System;
using System.Linq.Expressions;

class Test
{
    static void Main()
    {
        Expression<Func<DateTime, int>> dt = p => p.Minute;
        Console.WriteLine(dt);
    }
}

如果您将表达式类型更改为Expression<Func<DateTime,long>>,它会显示Convert(...)部分。我怀疑您需要更改Exclude(等等)方法的签名。

3
OT: yowser; about 4 days from 200k?翻译:OT:哇塞;距离20万还有大约4天? - Marc Gravell
@Marc:确实 - 如果我真的很幸运,就是3。 - Jon Skeet
@ahsteele:如果你只是识别一个属性,为什么需要UnaryExpression路径?那不就是只有MemberAccess吗?你真的需要处理强制转换吗? - Jon Skeet
我在想哪一个会先出现:10万个C#问题还是Jon Skeet获得20万分数;p - Marc Gravell
@Jon - 我在我的方法中最终得到的表达式是 {p => Convert(p.Minute)},其中 expression.Body{Convert(p.Minute)},它是一个 UnaryExpression。因此,我取出了 Operand,即 p.Minute,一个 MemberExpression,从那里我可以获取 Member.Name。最终,从我的视角来看,我正在采取 p => p.Minute 并将其沿着这个方式运行 ((expression.Body as UnaryExpression).Operand as MemberExpression).Member.Name。你是说有一种更“简洁”的方法来做到这一点,还是我完全没有理解你的意思? - ahsteele
显示剩余7条评论

4
你有没有计划使它更加灵活,或者这就是它所需要做的全部?
理想情况下,你应该拥有最简单的代码来完成你需要完成的任务,这样你就可以减少出错的可能性。
如果你正在进行概念验证,并且知道以后需要使用lambda表达式,那么保留它们是有意义的,但是,如果这是最终产品,那么前一种方法更容易阅读,而且如果其他人需要更改代码,也不太可能引起混淆。

我非常支持可读性,但使用lambda表达式的“代价”难道不值得类型安全吗? - ahsteele
1
@ahsteele - 你无论如何都要将其输出到字符串中,所以除了写入文件之外,如果你没有对这些值进行其他操作,类型安全性对你来说并没有什么用处。此外,如果你关心类型安全性,除了添加 lambda 表达式的复杂性之外,还有其他方法可以实现。保持简单是重要的部分。 - James Black
1
同意,但我说的是我正在使用的类的类型安全性。为了举例说明,我使用了DateTime,但如果我使用一个我控制并更改属性名称的值类型,lambda表达式将有所帮助。而魔术字符串版本会让我一直处于黑暗中,直到我后来发现了问题。我倾向于做可能最简单的事情,但想知道这是否是“正确”的答案。我有什么遗漏的吗? - ahsteele
@ahstelle - 你预见到是否有不使用 DateTime 的需要?这才是真正的问题。不要为了可能在未来需要而设计,这会导致问题。我在回答的第一句就问了这个问题。 - James Black
我明白你的意思。这将用于各种不同的值类型。假设我理解得“正确”,我甚至希望将最终产品开源在CodePlex或Google Code上。 - ahsteele

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