【问题标题】:C# Generics - Find correct concrete class depending on object typeC# 泛型 - 根据对象类型查找正确的具体类
【发布时间】:2011-01-17 11:56:03
【问题描述】:

我的域模型有几个 NewsType,它们是 NewsItem 的子类,如下所示(简化):

public abstract class NewsItem : Entity
{
    public virtual Account Account { get; set; }
    public virtual DateTime DateTime { get; set; }
}

这里有几个 NewsItem 的子类:

public class NewsItemJoiner : NewsItem
{
    public virtual Account AccountJoined { get; set; }
}

public class NewsItemStatus : NewsItem
{
    public virtual string Status { get; set; }
}

在我的 MVC 应用程序中,我想返回一个可能包含许多不同的 NewsItem 子类的 Newsitem 的集合。我现在需要做的是遍历每个新闻项,并从相关类中为该特定类型的 NewsItem 调用 Render 函数...代码可能会更容易解释:

public interface IRenderer<T> where T : NewsItem
{
    string Render(T item);
}

public class JoinedRenderer : IRenderer<NewsItemJoiner>
{
    public string Render(NewsItemJoiner item)
    {
        return String.Format("{0} has just joined our music network.", item.AccountJoined.ArtistName);
    }
}

public class StatusUpdateRenderer : IRenderer<NewsItemStatus>
{
    public string Render(NewsItemStatus item)
    {
        return String.Format("<span class='statusupdate'>{0}<span>", item.Status);
    }
}

我需要根据 NewsItem 的类型调用正确的类渲染函数。

【问题讨论】:

    标签: c# generics reflection inheritance interface


    【解决方案1】:

    对于虚函数来说,这似乎是一个相当明显的例子.....

    public abstract class RenderableNewsItem : NewsItem
    {
        abstract public string Render();
    }
    
    public class NewsItemStatus : RenderableNewsItem 
    { 
        public virtual string Status { get; set; } 
        public string Render() 
        { 
            return String.Format("<span class='statusupdate'>{0}<span>", this.Status); 
        } 
    }
    

    【讨论】:

    • 如果实际用法像 OP 的示例一样简单,那将是正确的,但在 SO 上并不常见。
    【解决方案2】:

    您可以创建一个字典,将 NewsItem 的类型用作键,并将 Render 函数用作值。或者,您可以维护所有具有 Render 函数的类的列表,或者只是所有 Render 函数的列表,并使用反射来确定应该使用哪种方法。但是,在我看来,您应该考虑重新设计应用程序,以便 NewsItem 抽象类本身具有虚拟 Render 函数,而不是执行任何这些操作。这将大大简化您的任务。

    编辑:以前认为 NewsItem 是一个界面。

    【讨论】:

      【解决方案3】:

      一种可能性:在启动时(即在与渲染代码相关的静态构造函数中),遍历程序集中的类并实例化并存储一个 Dictionary&lt;Type, object&gt;IRenderer&lt;T&gt;-实现实例映射到它们呈现的类型.

      (此建议假定渲染器对象是线程安全的,因为您最终可能会同时从多个请求线程调用 Render 方法。如果它们不是线程安全的,那么您需要将字典更改为&lt;Type, Type&gt; 并为每次使用实例化一个渲染器。)

      例如:

      public class RenderUtil
      {
          static Dictionary<Type, object> s_renderers;
      
          static RenderUtil()
          {
              s_renderers = new Dictionary<Type, object>();
      
              foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
              {
                  var renderInterface = type.GetInterfaces().FirstOrDefault(
                      i => i.IsGenericType && 
                           i.GetGenericTypeDefinition() == typeof(IRenderer<>));
      
                  if (renderInterface != null)
                  {
                      s_renderers.Add(
                          renderInterface.GetGenericArguments()[0],
                          Activator.CreateInstance(type));
                  }
              }
          }
      
          public static string Render<T>(T item)
          {
              IRenderer<T> renderer = null;
              try
              {
                  // no need to synchronize readonly access
                  renderer = (IRenderer<T>)s_renderers[item.GetType()];
              }
              catch
              {
                  throw new ArgumentException("No renderer for type " + item.GetType().Name);
              }
      
              return renderer.Render(item);
          }
      }
      

      用法:

      var newsItem = new NewsItemStatus();
      
      // in your example code, ends up calling StatusUpdateRenderer.Render:
      var rendered = RenderUtil.Render(newsItem);
      

      请注意,如果给定类型有多个渲染器,RenderUtil 类将在第一次使用时通过 TypeInitializationException 抛出 DuplicateKeyException

      【讨论】:

      • 我喜欢这个实现,我刚刚尝试过......它几乎可以工作,只是它正在寻找一个 NewsItem 类型的渲染器,而不是像 JoinerNewsItem 这样的正确子类。我正在使用 NHibernate 返回我的 NewsItem,它们被声明为 NewsItem,即使它们包含每个子类的附加属性......我需要以某种方式将它们向下转换为正确的类型。我知道向下转换不安全或最佳实践,但我认为这是唯一的方法?还是我错过了什么。谢谢!!
      • 好的,我通过使用 Dictionary 类型的字典解决了这个问题,而不是存储对象类型,而是使用 GetType().Name.ToString() 存储对象的名称非常感谢您的回答!
      • 很高兴能提供帮助。 (我已经更改了Render 方法以显示解决您遇到的问题的另一种方法。)
      【解决方案4】:

      我会做的是使用多个局部视图来呈现不同的NewsItem 子类。然后,我将在子类和部分视图名称之间进行某种映射。这里有两种方法:

      1. NewsItem 可以有一个虚拟字符串属性/方法,它返回与其关联的局部视图的名称。如果 NewsItem 是一个专门用于传递给视图的模型类,而不是 ORM 类或类似类,我只建议这样做。
      2. 在包含新闻项目列表的模型中,您可以有一个映射属性(例如 Dictionary&lt;Type, string&gt;)。

      设置完成后,您的视图可能如下所示:

      <% foreach (var newsItem in Model.NewsItems) { %>
          <%= Html.RenderPartial(newsItem.PartialViewName, newsItem) %>
      <% } >
      

      【讨论】:

      • 这是我在有更多时间后可能会考虑的另一个选项,但是我使用的是通过 AutoMapper 映射的强类型视图模型,这可能会增加一点复杂性。我喜欢这个的原因是我可以更好地控制使用部分视图输出的 HTMl 的格式。
      【解决方案5】:

      我刚刚做了这样的事情,但我手边没有代码。它使用了反射,如下所示:

      List<GenericContainer> gcList = new List<GenericContainer>();
      // GenericContainer can be a Jug, Bottle, Barrel, or just a GenericContainer type
      // [..fill it..]
      GenericContainer gc = gcList[i];
      Object returnvalue = gc.GetType()
                           .GetMethod("Pour", BindingFlags.Instance).Invoke(gc, null);
      

      【讨论】:

        【解决方案6】:

        考虑反转控制逻辑并在 NewsItem 中提供虚拟 Render() 方法。例如

        abstract class NewsItem {
            // ...
            public virtual string Render() { return string.Empty; }
        }
        

        然后你的子类可以根据需要实现:

        public class NewsItemJoiner : NewsItem
        {
            // ...
            public override string Render() {
                return String.Format("{0} has just joined our music network.", this.AccountJoined.ArtistName);
            }
        }
        

        编辑:
        替代技术
        关于 cmets 与其他人重新分离关注点的观点。我不知道您是否出于其他原因设置了 IRenderer,但如果不是,还有另一种不需要使用反射的技术。您可以改用访问者模式。

        首先你声明一个 NewsItemVisitor 类:

        public abstract class NewsItemVisitor
        {
            public abstract void Visit(NewsItemJoiner joiner);
            public abstract void Visit(NewsItemStatus status);
        }
        

        接下来,为 NewsItem 添加一个虚拟 Accept() 方法(在本例中,我已将您的数据类型更改为字符串,而不是 Account、Status 等):

        public abstract class NewsItem
        {
            public virtual string Account { get; set; }
            public virtual DateTime DateTime { get; set; }
            public abstract void Accept(NewsItemVisitor visitor);
        }
        
        public class NewsItemJoiner : NewsItem
        {
            public virtual string AccountJoined { get; set; }
            public override void Accept(NewsItemVisitor visitor)
            {
                visitor.Visit(this);
            }
        }
        
        public class NewsItemStatus : NewsItem
        {
            public virtual string Status { get; set; }
            public override void Accept(NewsItemVisitor visitor)
            {
                visitor.Visit(this);
            }
        }
        

        现在您可以创建一个具体的访问者,这是我们的渲染器:

        public class NewsItemListRenderer : NewsItemVisitor
        {
            private readonly List<NewsItem> itemList;
            private string renderedList = string.Empty;
        
            public NewsItemListRenderer(List<NewsItem> itemList)
            {
                this.itemList = itemList;
            }
        
            public string Render()
            {
                foreach (var item in itemList)
                {
                    item.Accept(this);
                }
        
                return renderedList;
            }
        
            public override void Visit(NewsItemJoiner joiner)
            {
                renderedList += "joiner: " + joiner.AccountJoined + Environment.NewLine;
            }
        
            public override void Visit(NewsItemStatus status)
            {
                renderedList += "status: " + status.Status + Environment.NewLine;
            }
        }
        

        如何呈现 NewsItem 实例列表的示例代码:

        List<NewsItem> itemList = new List<NewsItem>();
        itemList.Add(new NewsItemJoiner { AccountJoined = "fred" });
        itemList.Add(new NewsItemJoiner { AccountJoined = "pete" });
        itemList.Add(new NewsItemStatus { Status = "active" });
        itemList.Add(new NewsItemJoiner { AccountJoined = "jim" });
        itemList.Add(new NewsItemStatus { Status = "inactive" });
        
        NewsItemListRenderer renderer = new NewsItemListRenderer(itemList);
        Console.WriteLine(renderer.Render());
        

        运行它会得到以下输出:

        joiner: fred
        joiner: pete
        status: active
        joiner: jim
        status: inactive
        

        【讨论】:

        • 这种方式破坏了 MVC 的整体理念,不是吗?
        • 确实如此,但我也认为这是解决问题的最干净的方法。使用查找表和其他所有建议来构建复杂的解决方案似乎(值得商榷,可以肯定)只是为了强制执行 MVC 很愚蠢。
        • 这是我最初的设计,但我不相信我的域模型应该返回格式化的 HTML。我的 IRenderer 在我的控制器级别。
        • 当您有意分离代码层(即 MVC)之间的职责时,最干净的问题解决方案并不总是最好的。
        猜你喜欢
        • 1970-01-01
        • 2018-09-20
        • 2022-01-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-05-05
        • 2020-10-24
        • 1970-01-01
        相关资源
        最近更新 更多