如何简化重复if-then-assign构造?

Ser*_*lov 48 .net c# lambda

我有以下方法:

protected override bool ModifyExistingEntity(Product entity, ProductModel item)
{
    bool isModified = false;

    if (entity.Title != item.Title)
    {
        isModified = true;
        entity.Title = item.Title;
    }

    if (entity.ServerId != item.Id)
    {
        isModified = true;
        entity.ServerId = item.Id;
    }

    return isModified;
}
Run Code Online (Sandbox Code Playgroud)

我想知道你是否可以建议一种更好的方法来实现该方法.

问题很明显:每个属性有5行几乎复制粘贴的代码太多了.可能是我的愿景中使用Func-s/Expression-s 的解决方案.

bst*_*zel 47

你有一个时间耦合的情况,即你正在混合检查实体是否随着分配而改变.如果将两者分开,则代码变得更清晰:

protected override bool ModifyExistingEntity(Product entity, ProductModel item)
{
    bool isModified = this.IsEntityModified(entity, item);

    if (isModified)
    {
        this.UpdateEntity(entity, item);
    }

    return isModified;
}

private bool IsEntityModified(Product entity, ProductModel item)
{
    return entity.Title != item.Title || entity.ServerId != item.ServerId;
}

private void UpdateEntity(Product entity, ProductModel item)
{
    entity.Title = item.Title;
    entity.ServerId = item.Id;
}
Run Code Online (Sandbox Code Playgroud)

做任何聪明和时髦的东西(TM)Func<>或类似的东西,在这种情况下似乎没有帮助,因为它不会清楚地传达你的意图.

  • 使用此解决方案,您必须使用两种不同的方法执行维护,而不是几条靠近的方法. (5认同)
  • 我不确定你在说什么.我说使用此代码会更难维护(添加/删除属性),因为属性访问分散在两个方法上. (5认同)
  • 我很难理解在一些抽象的可维护性方面故意使代码不那么清晰的概念.在我的书中,更清晰的代码总是更易于维护.如果你真的害怕像所描述的那样搞变化,那么你可能不会以测试为导向.不过你应该这样. (5认同)
  • 这取决于......首先我认为它使代码更清晰:我只更新了Object,如果它被修改了!这听起来非常直观.第二:如果在对象上调用setter有任何副作用,则不会调用它们.如果有一些计算值在分配中重新计算,我们会跳过它们.因此,如果我有一个包含1000个对象的视图状态,只有一个被更改,我为所有对象调用它,它可以产生很大的性能差异! (3认同)
  • @bstenzel你是对的,代码不那么清晰,更脆弱.话虽如此,我认为如果意图是保持完全相同的方法行为,这可能是最好的方法.事实上,代码在改进之后仍然看起来很奇怪,这需要超越方法.例如,为什么要将实体传递给方法,并且该方法的目的是更改其属性?或者,为什么更新仅在实体已更改时才会发生?可能有更深的气味. (3认同)
  • @Andy不,在添加单个属性时,您不必阅读整个DTO类.我的观点是:代码开始时是非常难以维护的,而且这种方法并没有完全改善.这里的所有示例对于两个或三个属性都非常好,但是包含20个这样的属性的类会使那些不熟悉代码库的人或者几周没有看过它的人感到困惑.然后很容易忘记添加`|| entity.NewProperty!= item.NewProperty`到`IsEntityModified()`方法.就像@moarboilerplate所说,这个问题的核心问题是_"为什么这样?"_. (2认同)

Sri*_*vel 12

这样的事情应该有效

protected bool ModifyExistingEntity(Person entity, ProductModel item)
{
    bool isModified = CompareAndModify(() => entity.Title = item.Title, () => entity.Title != item.Title);
    isModified |= CompareAndModify(() => entity.ServerId = item.Id, () => entity.ServerId != item.Id);

    return isModified;
}

private bool CompareAndModify(Action setter, Func<bool> comparator)
{
    if (comparator())
    {
        setter();
        return true;
    }
    return false;
}
Run Code Online (Sandbox Code Playgroud)

不确定这是否可读.这是主观的.

  • 这应该工作.我绝对不希望在我的代码库中看到它; P (15认同)
  • @Guillaume我同意.读者将加倍在那里.如果您担心可读性,我建议[bstenzel的回答](http://stackoverflow.com/a/29913912/2530848).我根据OP的回答解决了这个问题"问题很明显:每个属性有5行几乎复制粘贴的代码太多了" (3认同)

Pat*_*man 12

我认为这个答案的扩展可能适合你:

public static bool SetIfModified<CLeft, T>(Expression<Func<CLeft, T>> exprLeft, CLeft leftType, T rightValue)
{
    var getterLeft = exprLeft.Compile();

    if (EqualityComparer<T>.Default.Equals(getterLeft(leftType), rightValue))
    {
        var newValueLeft = Expression.Parameter(exprLeft.Body.Type);
        var assignLeft = Expression.Lambda<Action<CLeft, T>>(Expression.Assign(exprLeft.Body, newValueLeft), exprLeft.Parameters[0], newValueLeft);

        var setterLeft = assignLeft.Compile();

        setterLeft(leftType, rightValue);
        return true;
    }
    else
    {
        return false;
    }
}
Run Code Online (Sandbox Code Playgroud)

它需要一个表达式来检查值.它动态编译和执行它.

像这样使用它:

public class Product { public string Title { get; set; } }
public class ProductModel { public string Title { get; set; } }

static void Main(string[] args)
{
    Product lc = new Product();
    ProductModel rc = new ProductModel();
    rc.Title = "abc";
    bool modified = SetIfModified(l => l.Title, lc, r.Title);

    // modified is true
    // lc.Title is "abc"

}
Run Code Online (Sandbox Code Playgroud)

  • `Expression.Compile`很贵.因此,您可能需要缓存已编译的表达式,以防有人使用此方法. (2认同)

Kob*_*obi 10

使用T4进行元编程

另一种方法 - 通常当我们有重复的代码时,实际上很简单,可能非常快.在这种情况下,每个重复的if块都不相同 - 它拥有一点知识 - 从一个属性到另一个属性的映射.

编写维护重复的块很烦人.
避免编写有用的重复代码的一种方法是自动生成它.

使用我的解决方案,映射非常简单:

var mappings = new []{
    new Mapper("ProductModel", "Product")
    { 
        "Title",               // ProductModel.Title goes to Product.Title
        {"Id", "ServiceId"},   // ProductModel.Id goes to Product.ServiceId
    },
};
Run Code Online (Sandbox Code Playgroud)

这是一个文本模板(Visual Studio的内置功能):

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    // Consider including the namespace in the class names.
    // You only need to change the mappings.
    var product = new Mapper("Product", "ProductEntity") { "Name", {"Id", "ServiceId"} };
    var person = new Mapper("Person", "DbPerson") { "Employee", {"Name", "FullName"}, {"Addredd", "HomeAddress"} };

    var mappings = new [] {product, person};
#>
// !!!
// !!!  Do not modify this file, it is automatically generated. Change the .tt file instead.     !!!
// !!!
namespace Your.Mapper
{
    partial class Mapper
    {
        <# foreach(var mapping in mappings) { 
        #>/// <summary>
        /// Set <paramref name="target"/> properties by copying them from <paramref name="source"/>.
        /// </summary>
        /// <remarks>Mapping:<br/>
        <#foreach(var property in mapping){
        #>/// <see cref="<#=mapping.SourceType#>.<#=property.SourceProperty#>"/> ? <see cref="<#=mapping.TargetType#>.<#=property.TargetProperty#>"/> <br/>
        <#}
        #>/// </remarks>
        /// <returns><c>true</c> if any property was changed, <c>false</c> if all properties were the same.</returns>
        public bool ModifyExistingEntity(<#=mapping.SourceType#> source, <#=mapping.TargetType#> target)
        {
            bool dirty = false;
            <# foreach(var property in mapping) {
            #>if (target.<#=property.TargetProperty#> != source.<#=property.SourceProperty#>)
            {
                dirty = true;
                target.<#=property.TargetProperty#> = source.<#=property.SourceProperty#>;
            }           
            <#}
            #>return dirty;
        }
        <#
         } 
        #>

    }
}

<#+
class Mapper : IEnumerable<PropertyMapper>
{
    private readonly List<PropertyMapper> _properties;

    public Mapper(string sourceType, string targetType)
    {
        SourceType = sourceType;
        TargetType = targetType;
        _properties = new List<PropertyMapper>();
    }

    public string SourceType { get; set; }
    public string TargetType { get; set; }

    public void Add(string fieldName)
    {
        _properties.Add(new PropertyMapper {SourceProperty = fieldName, TargetProperty = fieldName});
    }

    public void Add(string sourceProperty, string targetProperty)
    {
        _properties.Add(new PropertyMapper { SourceProperty = sourceProperty, TargetProperty = targetProperty });
    }

    IEnumerator<PropertyMapper> IEnumerable<PropertyMapper>.GetEnumerator() { return _properties.GetEnumerator(); }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return _properties.GetEnumerator(); }
}

class PropertyMapper
{
    public string SourceProperty { get; set; }
    public string TargetProperty { get; set; }
}
#>
Run Code Online (Sandbox Code Playgroud)

此模板生成以下代码:https://gist.github.com/kobi/d52dd1ff27541acaae10

好处:

  • 繁重的工作在编译时完成(实际上在编译时间之前一次) - 生成的代码很快.
  • 生成的代码已记录在案.
  • 易于维护 - 您可以在一个点上更改所有映射器.
  • 生成的方法已记录在案.
  • 没有复制粘贴错误.
  • 这很有趣.

缺点:

  • 使用字符串获取属性名称.请记住 - 这不是生产代码,它只是用于生成代码.可以使用真实类型和表达式树(下面的示例).
  • 静态分析可能会错过模板中的用法(即使我们使用表达式,并非所有工具都会查看tt文件).
  • 很多人不知道发生了什么.
  • 如果您使用的是表达式,那么引用您的类型是很棘手的.

笔记:

  • 我命名的参数sourcetarget,并改变它们的顺序所以source永远是第一位.

有人担心我使用的是字符串而不是真正的属性.虽然在这种情况下这是一个小问题(编译输出),但这里有一个适用于您的真实对象的附加功能.

在顶部,添加此(第三个应该是您的命名空间):

<#@ assembly name="$(TargetPath)" #>
<#@ import namespace="System.Linq.Expressions" #>
<#@ import namespace="ConsoleApplicationT4So29913514" #>  
Run Code Online (Sandbox Code Playgroud)

在底部,添加:

class Mapper<TSource, TTarget> : Mapper
{
    public Mapper()
        : base(typeof(TSource).FullName, typeof(TTarget).FullName)
    {

    }

    private static string GetExpressionMemberAccess(LambdaExpression getProperty)
    {
        var member = (MemberExpression)getProperty.Body;
        //var lambdaParameterName = (ParameterExpression)member.Expression;
        var lambdaParameterName = getProperty.Parameters[0]; // `x` in `x => x.PropertyName`
        var labmdaBody = member.ToString();
        //will not work with indexer.
        return labmdaBody.Substring(lambdaParameterName.Name.Length + 1); //+1 to remove the `.`, get "PropertyName"
    }

    public void Add<TProperty>(Expression<Func<TSource, TProperty>> getSourceProperty, Expression<Func<TTarget, TProperty>> getTargetProperty)
    {
        Add(GetExpressionMemberAccess(getSourceProperty), GetExpressionMemberAccess(getTargetProperty));
    }

    /// <summary>
    /// The doesn't really make sense, but we assume we have <c>source=>source.Property</c>, <c>target=>target.Property</c>
    /// </summary>
    public void Add<TProperty>(Expression<Func<TSource, TProperty>> getProperty)
    {
        Add(GetExpressionMemberAccess(getProperty));
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

var mappings = new Mapper[] {
    new Mapper<Student,StudentRecord>
    {
        {s=>s.Title, t=>t.EntityTitle},
        {s=>s.StudentId, t=>t.Id},
        s=>s.Name,
        s=>s.LuckyNumber,
    },
    new Mapper<Car,RaceCar>
    {
        c=>c.Color,
        c=>c.Driver,
        {c=>c.Driver.Length, r=>r.DriverNameDisplayWidth},
    },
};
Run Code Online (Sandbox Code Playgroud)

整个文件应如下所示:https://gist.github.com/kobi/6423eaa13cca238447a8
输出看起来仍然相同:https://gist.github.com/kobi/3508e9f5522a13e1b66b

笔记:

  • 表达式仅用于将属性名称作为字符串,我们不编译它们或运行它们.
  • 在C#6中,我们将使用nameof()运算符,这是表达式和​​无魔术字符串之间的一个很好的折衷.


Roy*_*tus 8

没有魔术棒来简化这一点.

您可以让实体本身提供IsModified属性,然后由属性设置者设置,例如:

public string Title {
   get { return _title; }
   set {
         if (value != _title)
         {
             _title = value;
             IsModified = true;
         }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果这是太多的工作,你的解决方案是好的.

  • 如果你必须修改实体,你可能最好不要实现INotifyPropertyChanged,然后处理该事件并在发生属性更改时设置isModified = true. (5认同)

Gui*_*ume 7

如果您想让它可读,您可以为此目的创建一个类,使用非常简单,避免重复代码:

protected override bool ModifyExistingEntity(Product entity, ProductModel item)
{
    return new Modifier<Product>(entity)
               .SetIfNeeded(e => e.Title, item.Title);
               .SetIfNeeded(e => e.ServerId, item.Id);
               .EntityWasModified;
}
Run Code Online (Sandbox Code Playgroud)

执行:

我从Patrick Hofman那里获取了一些代码,用getter表达式生成一个setter.

public class Modifier<TEntity>
{    
    public Modifier(TEntity entity)
    {
        Entity = entity;
    }

    public TEntity Entity { get; private set; }

    public bool EntityWasModified { get; private set; }

    public Modifier<TEntity> SetIfNeeded<TProperty>(Expression<Func<TEntity, TProperty>> entityPropertyGetter, TProperty modelValue)
    {
        var getter = entityPropertyGetter.Compile();
        var setter = GetSetterExpression(entityPropertyGetter).Compile();

        if (!object.Equals(getter(Entity), modelValue))
        {
            setter(Entity, modelValue);
            EntityWasModified = true;
        }
        return this;
    }

    private static Expression<Action<TEntity, TProperty>> GetSetterExpression(Expression<Func<TEntity, TProperty>> getterExpression)
    {
        var newValue = Expression.Parameter(getterExpression.Body.Type);

        return Expression.Lambda<Action<TEntity, TProperty>>(
            Expression.Assign(getterExpression.Body, newValue),
            getterExpression.Parameters[0], newValue);
    }
}
Run Code Online (Sandbox Code Playgroud)

您可能希望缓存结果.Compile以提高性能.


归档时间:

查看次数:

3446 次

最近记录:

10 年,4 月 前