【问题标题】:save T in inner extensions在内部扩展中保存 T
【发布时间】:2015-10-28 18:19:32
【问题描述】:

我有两个扩展:

public static TModel ObjectToModel<TModel>(this object source, string[] propsToExclude = null)
        where TModel : new()
{
   var dest = new TModel();
   source.CopyPropertiesToObject(dest, propsToExclude);
   return dest;
}

public static bool CopyPropertiesToObject<T, TU>(this T source, TU dest, string[] propsToExclude = null)
{
   propsToExclude = propsToExclude ?? new string[0];
   var sourceProps = typeof(T).GetProperties().Cast<PropertyInfo>();
   var destProps = typeof(TU).GetProperties().Cast<PropertyInfo>().Where(x => x.CanWrite && sourceProps.Any(s => s.Name == x.Name) && !propsToExclude.Any(e => e == x.Name));
   // code
 }

使用:

source.CopyPropertiesToObject(dest); // fine
dest = source.ObjectToModel<TModel>(); // not work

这不起作用,因为ObjectToModel 方法中的source 参数是object 类型 - 所以CopyPropertiesToObject 方法中的var sourceProps = typeof(T).GetProperties()... 返回一个空的属性列表。

如何更改ObjectToModel&lt;TModel&gt; 扩展才能正常工作?

【问题讨论】:

  • “不起作用”是什么意思?你能提供一些输入示例吗?
  • @IanR.O'Brien 在由ObjectToModel 调用的CopyPropertiesToObject 方法中,typeof(T) 将返回System.Object,而不是source 变量的实际类型。因此,不会复制任何属性。
  • doesn't work because ..ObjectToModel&lt;TModel&gt;(this object source.. 这条线是什么意思?请使用完整的句子。
  • @M.kazemAkhgary 另一个用户的编辑删除了对源参数类型的强调。
  • 你为什么用typeof(T)而不是source.GetType()? GetType 将为您提供正确的对象类型,无论您传递的是基对象还是派生对象;相反,typeof(T) 给出了基本类型,因此设计上是错误的。

标签: c# static-methods typeof


【解决方案1】:

那么,让我们把它全部分解吧。

我们在这里做什么?

显然我们正在将数据从对象 Ao 复制到对象 Bo。更准确地说:我们将在类型 A 和类型 B 之间相交的属性从对象 Ao 复制到 Bo。如果一个属性不在这两种类型中,我们就不会复制。

假设类型 A 和类型 B 在编译时是已知的。然而:

这不起作用,因为 ObjectToModel 方法中的源参数是对象类型

我们需要什么?

基本上我们需要运行时类型信息,我们需要获取属性信息(反射)。请注意,这里对对象没有接口约束(我们只知道它可以被实例化)——基本上这意味着我们只是使用我们知道在object 中的东西。我们唯一没有的就是类型。

让我们为了参数添加一些代码:

public class Named 
{
  public string Name { get; set; }
}

public class Foo : Named
{
  public int MyProp { get; set; }
}

public class Bar : Named
{
  public int MyProp { get; set; }
  public int MySecondProp { get; set; }
}

// ...

Bar SomeMethod(Named source) 
{
  // code, code, code...
  return source.ObjectToModel<Bar>();
}

这里的问题是:需要发生什么? MyProp 应该被复制吗?

由于它的定义方式,我会这么认为。我想这也是为什么object 首先出现在方法的签名中。

如何解决?

好吧,如果你这样做,唯一需要的就是获取类型 Foo 以便我们可以获取属性。实现相当简单:

public static TModel ObjectToModel<TModel>(this object source, string[] propsToExclude = null)
        where TModel : new()
{
   var dest = new TModel();
   CopyPropertiesToObject(source, dest, propsToExclude);
   return dest;
}


public static bool CopyPropertiesToObject(this object source, object dest, string[] propsToExclude = null)
{
   propsToExclude = propsToExclude ?? new string[0];
   var sourceProps = source.GetType().GetProperties().Cast<PropertyInfo>();
   var destProps = dest.GetType().GetProperties().Cast<PropertyInfo>().Where(x => x.CanWrite && sourceProps.Any(s => s.Name == x.Name) && !propsToExclude.Any(e => e == x.Name));
   // code
}

如您所见,我已完全从事物中删除了通用 T。作为奖励,GetType 使用 vtable 查找(继承)来获取 source 对象的最派生类型,在本例中为 Foo 而不是 Named

但是……使用泛型不是对性能有好处吗?

@AsadSaeeduddin 表示使用 typeof(T) 比 GetType '更好',因为该信息在编译时是已知的。让我们在这里深入了解细节。

从开发人员的角度来看,如果我们将对象作为通用模板传递,我们确实知道对象具有哪些类型和属性。但是,这不是 .NET 编译器和运行时的工作方式。

Typeof 在 .NET 中作为ldtoken IL 指令实现,它基本上获得了类型标记。在这种情况下,要获得令牌的是通用参数T。此时,您必须意识到泛型不是 C++ 模板。更具体地说,代码是基于每个方法的 JIT,使用约束的细节作为使用和调用方法的方式。我会举例说明。

假设我们有 Name、Foo 和 Bar 类。所有这些类都有一个(虚拟/覆盖)方法“GetName”,我们在这样的泛型中调用它:

// Note: if Named was an interface, you should add 'class' as constraint!
public static string FindName<T>(this T source) where T : Named
{
  return source.GetName();
}

幕后发生的事情是该方法是由 JIT 编译的。在这一点上,我们确定 T 的所有实例都是(大小相等的)指针(因此(隐藏)class 约束)并且它们都是Named 的实例。它不像模板那样扩展,所以如果我们将它翻译成 C++ 等价物,它会像这样工作:

static string FindName(Named* source)
{
  return source->GetName();
}

其实由此可以推断,编译出来的汇编代码其实和:

public static string FindName(this Named source)
{
  return source.GetName();
}

这里与问题的唯一“真正”区别是您可以使用typeof(T) 转换为ldtoken。显然,这为每个T 提供了不同的结果。此时,.NET 可以选择 2 种不同的实现:

  1. 为每个不同的T重新编译方法。
  2. 呼叫source.GetType()非虚拟。

我不会在这里详细介绍如何实际测试它,但选项 (2) 是 .NET 团队决定做的。这条规则只有一个例外:如果 T 是值类型,则每种类型 T 对堆栈处理方式的影响不同(记住,您必须将参数放在堆栈上),这意味着方法必须重新-编译。如果你仍然对如何测试这个感兴趣,这是我一年前的一个问题。

所以泛型和非泛型一样快?

不,并非总是如此。例如,考虑IEquality&lt;T&gt; 接口:

bool Equals(T other);

一个典型的实现是:

bool Equals(Foo other) { return other.MyProp == MyProp; }

如果你考虑替代方案 (object.Equals),是这样的:

bool Equals(object other) { return other != null && other is Foo && ((Foo)other).MyProp == MyProp; }

请注意,速度的差异来自于我们可以假设更多的事实,从而简化了编译器必须做的工作。

回到问题

那么让我们来看看这里的底线:

  • 在此处使用或不使用泛型对性能没有任何实际影响。
  • 使用泛型为您提供您正在使用的类型,而不是最派生的类型。从签名来看,我希望后者是目标。

【讨论】:

  • 非常详细的答案。但后来我问你为什么在 CopyPropertiesToObject 中保存泛型??
  • 因为您需要调用 new,这是约束的一部分。基本上这是唯一的原因。
  • ...哦,哎呀,是的,我也应该删除它。你只需要 TModel。
  • 您提供的代码无法编译。您需要指定一个class/struct 约束一个特定的类,而不是两者。
  • @AsadSaeeduddin 我是在记事本中从头顶写的,可能有一些错误;不过,整个故事仍然是正确的。原因是 Named 已经是一个类(所以它不能是一个结构),所以它不能编译。通常我使用接口;如果 Named 是一个接口,它就会被编译。刚刚修好了。
【解决方案2】:

只需将要复制的对象的类型捕获为类型参数即可。在下面的 sn-p 中,我添加了与第一个参数的类型对应的TSource 类型参数。

public static TModel ObjectToModel<TSource, TModel>(this TSource source, string[] propsToExclude = null)
    where TModel : new()
{
    var dest = new TModel();

    source.CopyPropertiesToObject(dest, propsToExclude);

    return dest;
}

【讨论】:

  • 最初的问题是什么?如何解决?我并不怀疑你——只是在代码之外寻找一些清晰度,因为我发现这是一个有趣的问题。
  • @Asad Saeeduddin,我先试了一下,得到一个错误“要使用通用方法“DataMng.M.ObjectToModel(TSource, string[])”需要两个类型的参数"
  • @Qio 你怎么称呼它?
  • @Asad Saeeduddin,如主题:dest = source.ObjectToModel();
  • @Qio 您还需要提供TSource。无论source 变量是什么类型,都应该是第一个类型参数,TModel 应该是第二个。
【解决方案3】:

我认为问题是由将源作为对象而不是实际类型装箱引起的。所以像这样的东西会做映射。

 public static class Extensions
    {
        public static TModel ObjectToModel<TSource, TModel>(this TSource source, string[] propsToExclude = null)
            where TModel : new()
        {
            var dest = new TModel();

            source.CopyPropertiesToObject(dest, propsToExclude);

            return dest;
        }

        public static bool CopyPropertiesToObject<T, TU>(this T source, TU dest, string[] propsToExclude = null)
        {
            try
            {
                propsToExclude = propsToExclude ?? new string[0];

                var sourceProps = typeof (T).GetProperties().Cast<PropertyInfo>().ToArray();
                var destProps =
                    typeof (TU).GetProperties()
                        .Cast<PropertyInfo>()
                        .Where(
                            x =>
                                x.CanWrite && sourceProps.Any(s => s.Name == x.Name) &&
                                propsToExclude.All(e => e != x.Name)).ToArray();

                foreach (var property in destProps)
                {
                    var sourceProperty = sourceProps.FirstOrDefault(c => c.Name == property.Name);
                    if (source != null)
                    {
                        var value = sourceProperty.GetValue(source);
                        property.SetValue(dest, value);
                    }
                }
            }
            catch
            {
                return false;
            }
            return true;
        }
    }

    [TestClass]
    public class Scratch
    {
        [TestMethod]
        public void PropertiesMapAsExpected()
        {
            var model = new Model {Id = 2, Name = "foo"};
            var viewModel = model.ObjectToModel<Model, ViewModel>();
            Assert.AreEqual(model.Id, viewModel.Id);
            Assert.AreEqual(model.Name, viewModel.Name);
        }

        public class Model
        {
            public int Id { get; set; }
            public string Name { get; set; }

            public int NotMapped { get; set; }
        }

        public class ViewModel
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }
    }

请记住,当您想要开始映射具有不同名称、集合、嵌套对象的属性时,尝试动态映射类通常会变得越来越复杂,并且存在与反射相关的一些性能损失,尽管不像过去那么糟糕是。

您可能想查看automapper,它几乎适用于所有 .net 版本,经过高度优化和可配置。

【讨论】:

  • 我先试了一下,报错“To use a generic method "DataMng.M.ObjectToModel(TSource, string[])" requires two arguments of type"
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-16
  • 1970-01-01
  • 1970-01-01
  • 2011-02-02
相关资源
最近更新 更多