【问题标题】:Effects of returning a self reference from an instance method in C#从 C# 中的实例方法返回自引用的效果
【发布时间】:2013-11-01 17:20:30
【问题描述】:

假设我有一个名为 IConvertableModel 的接口,它可以帮助我将一些 MVC 模型与 DTO 对象相互转换,如下所示:

public class DisplayEditModel : IConvertableModel<Display>
{
    [HiddenInput(DisplayValue = false)]
    public int ObjectId { get; set; }

    [StringLength(255)]
    public string Description { get; set; }

    public Display ToDto()
    {
        return new Display
        {   
            Description = Description,
            ObjectId = ObjectId,
        };
    }

    public void SetFromDto(Display dto)
    {
        Description = dto.Description;
        ObjectId = dto.ObjectId;
    }
}

但是这种方法有一个问题,那就是它不允许我这样做:

var dto = _dtoRepository.GetFirstDto();
return new DisplayEditModel().SetFromDto(dto);

相反,我应该执行以下操作:

var dto = _dtoRepository.GetFirstDto();
var model = new DisplayEditModel();
model.SetFromDto(dto);
return model;

从长远来看,这会增加额外的两行代码和一点点复杂性。

我的想法是将SetFromDto 方法转换成这样的:

public DisplayEditModel SetFromDto(Display dto)
{
   Description = dto.Description;
   ObjectId = dto.ObjectId;
   return this;
}

我认为这段代码的好处是显而易见的,但我也想了解这是否会损害代码的可读性,并从长远来看会给开发人员带来意想不到的结果,如果你有其他想法,你会推荐什么。

注意:由于接口的原因,我没有考虑实现构造方法。

【问题讨论】:

  • 听起来理想的方法是静态工厂或构造函数,但不幸的是,我认为这两种方法都不能实现接口强制。
  • @David,你是对的,对的,对的。
  • 这不正是 AutoMapper 的发明目的吗?为什么要自己动手?
  • @DanM:因为 AutoMapper 有其邪恶的一面。当您更改某些内容时,它不会给您适当的编译时错误,并且会在运行时崩溃。所以我想明确指定要映射的内容并获得良好的旧编译时异常。

标签: c#


【解决方案1】:

一些想法,首先:

  1. 添加代码行与添加复杂性不同。拥有三个语句,每个语句执行一个简单的操作,并不一定比其中包含三个操作的单个语句更难维护或理解。
  2. 当一个方法以Set... 开头时,程序员会自动假设目标对象的一些有状态值会被这个方法改变。 Set 方法很少有返回值。 C# 中的属性设置器实际上“返回”传递给它们的原始值,因此您可以链接设置器:

    int i = foo.A = 2;
    

    因此,先例通常是反对专门从 set 方法返回“this”。

  3. 当您期望一个接一个地执行多个操作时,通常链接是最有用/最理想的。例如,C# 提供了很好的初始化语法,因此您可以在同一个对象上“链接”一堆不同的属性设置器:

    var foo = new Foo { A = 1, B = 2 };
    

    您可以看到链接如何满足执行类似、分组、重复操作的需求,这些操作通常会一起执行。这不是您要解决的问题。

如果您的主要抱怨是您不喜欢拥有三行代码,为什么不直接使用一个帮助器,其名称表明您正在尝试做什么?

TModel MapToModel<TModel, TDto>(TDto dto, TModel model)
    where TModel : IConvertableModel<TDto>
{
    model.SetFromDto(dto);
    return model;
}

// usage:

var dto = _dtoRepository.GetFirstDto();
return MapToModel(dto, new DisplayEditModel());

...甚至:

TModel CreateModel<TModel, TDto>(TDto dto)
    where TModel : IConvertableModel<TDto>, new()
{
    var model = new TModel();
    return MapToModel(dto, model);
}

// usage:

var dto = _dtoRepository.GetFirstDto();
return CreateModel<DisplayEditModel>(dto);

这简单、易读且可行,而您建议的方法会破坏IConvertableModel&lt;Display&gt; 接口:

public interface IConvertableModel<TDto>
{
    public TDto ToDto();
    public ??? SetFromDto(TDto dto);
}

SetFromDto 会返回什么?您必须在 IConvertableModel 上定义另一个泛型类型。

public interface IConvertableModel<TDto, TModel> {
    public TDto ToDto();
    public TModel SetFromDto(TDto dto);
}

但这并不表示SetFromDto 方法一定会返回自身,因为它允许不是TModel 的类实现IConvertableModel 在两个其他类型。

现在,您可以不遗余力地将泛型推得更远:

public interface IConvertableModel<TDto, TModel>
    where TModel : IConvertableModel<TDto, TModel>
{...}

但这仍然允许一些捏造,并且接口不能保证你真的返回“this”对象。总而言之,我不太喜欢这种方法。

【讨论】:

  • 感谢您的一步一步和非常详细的回答:)
【解决方案2】:

而不是让DisplayEditModel 拥有一个获取/设置Display 对象的方法 来获取/设置值,只需使用一个属性实际上有一个单独的后备存储:

public Display Display
{
    get
    {
        return new Display
        {
            Description = Description,
            ObjectId = ObjectId,
        };
    }
    set
    {
        Description = value.Description;
        ObjectId = value.ObjectId;
    }
}

现在您可以在创建模型时使用带有此属性的对象初始化器:

return new DisplayEditModel() { Display = dto };

【讨论】:

  • 这是一个由 MVC 向下传递到客户端的模型。所以我不想介绍另一个根本不会被 MVC 模型绑定器映射的属性。
  • @Tarik 为什么不呢?您可以选择不绑定所有属性。
  • @Tarik 另一个选项是创建一个构造函数,该构造函数接受一个Display 对象,以及一个无参数构造函数。如果所有这些都是一个问题,那么只需创建一个工厂方法,如前所述,它创建一个对象并设置 Display 然后返回它。
  • 因为它给开发人员带来了另一个关于映射什么和不映射什么的问题。 Model 类中的任何内容都应该用于映射,至少这是我所相信的,因为这就是我们拥有模型类的原因。
  • @Servy:MVC 中的 ViewModels 通常装饰有自定义属性,以向编辑器模板提供提示,例如:重复数据或业务层对象的属性可能是必要且有意的,以便提供附加这些属性的位置。
【解决方案3】:

这是一种非常 javascript 的方法来解决这个问题,尽管它有它的好处。在 C# 的上下文中,虽然 LINQ 等库这样做是为了允许将函数调用链接在一起,但这有点奇怪。

我唯一担心的是,这必须是一个始终如一的类。实现链接函数返回模式并不是一种设计选择,而是一种方便。在这种情况下要遵循的规则是,每次更改对象时都返回 this

在性能方面,链接也可能不值得。通过将所有这些操作包装到一个函数中可以完成的事情要快得多。例如:

   MyVector.setX(1).SetY(1).SetZ(1).SetW(0)

比简单的慢很多

   MyVector.set(1, 1, 1, 0)

因为现在您正在执行过多的堆栈操作来做一些相当简单的事情。只有在占用大量计算时间并且有意义链接在一起的非常大的操作上才值得。因此,LINQ 允许您将事物链接在一起。

我不会说它有必要“伤害”或危险。我们处于托管语言的世界中,因此我们无法直接访问该内存位置(与 C/C++ 不同)。所以我只称它为一种设计选择,在某些情况下可能相当强大,而在其他情况下则不是那么强大。

【讨论】:

  • 当你有不可变的对象时,链接通常是有意义的。返回一个全新的对象,然后用这个新对象做一些事情来创建一个新对象是有意义的,而且真的没有任何好的方法来处理不可变对象除了。您可以将中间对象存储在变量中,但重点是什么。不过,对于可变对象而言,链接的意义要小得多。
  • 向量是种类使用这种方法的可变数据的一个很好的例子。因为我们谈论的是代数运算,所以您希望每次做某事时都获得一个新副本,例如,在不影响原始版本的情况下获得规范化版本。这些通常是通过复制返回来完成的(结构在 C# 中提供了这种行为)。然而,它们仍然是可变对象。正如我之前提到的,集合(列表、数组)linq 操作返回this,因此您可以将这些操作链接在一起。
  • “一种非常 javascript 的方式...” 它可能更像是一种“非常 jQuery 的方式”。 Javascript 本身并没有或多或少地给你链接的理由,但是 jQuery 利用链接可以轻松地对一堆 DOM 元素执行一系列操作。 C# 在使用 LINQ 操作时倾向于链接,正如 Servy 指出的那样,每个操作都会根据前一个操作生成一个新的不可变对象。
【解决方案4】:

如前所述,可链接的方法可以正常工作,但在 C# 中不像在其他一些语言中那样常见。如果额外的代码行只发生在一个地方,我就不要管它了。如果它真的困扰着你或者你经常这样做,那么考虑为它实现一个特殊的构造函数:

public void DisplayEditModel(Display dto)
{
    this.SetFrom(dto);
}

或静态工厂方法:

public static DisplayEditModel CreateFrom(Display dto)
{
    var model = new DisplayEditModel();
    model.SetFrom(dto);
    return model;
}

任何一个选项都有一个明确的意图,让您可以在一行中创建和返回对象,并且是惯用的。它确实需要在DisplayEditModel 中增加几行代码,但我怀疑这会是一个严重的问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-12-17
    • 1970-01-01
    • 2018-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-11
    • 1970-01-01
    相关资源
    最近更新 更多