【问题标题】:A design problem involving Collections, Generics and Interfaces in C#C#中涉及集合、泛型和接口的设计问题
【发布时间】:2011-03-21 17:59:09
【问题描述】:

这篇文章包含很多代码,但如果你能阅读并理解它,我将不胜感激......并希望提出解决方案

假设我正在构建一个网络游戏,其中需要绘制实体并相应地从服务器更新其中一些实体。

Drawable 类负责在屏幕上绘制实体:

class Drawable
{
    public int ID { get; set; } // I put the ID here instead of having another 
                                // class that Drawable derives from, as to not 
                                // further complicate the example code.

    public void Draw() { }
}

从服务器接收的数据实现IData,每个具体的IData 具有不同的属性。假设我们有以下数据,PlayerEnemy 将收到:

interface IData
{
    int ID { get; set; }
}

class PlayerData : IData
{
    public int ID { get; set; }
    public string Name { get; set; }
}

class EnemyData : IData
{
    public int ID { get; set; }
    public int Damage { get; set; }
}

应该可以从服务器更新的游戏实体实现IUpdateable<T>,其中TIData

interface IUpdateable<T> where T : IData
{
    void Update(T data);
}

因此,我们的实体是DrawableUpdateable

class Enemy : Drawable, IUpdateable<EnemyData>
{
    public void Update(EnemyData data) { }
}

class Player : Drawable, IUpdateable<PlayerData>
{
    public void Update(PlayerData data) {}
}

这就是游戏的基本结构。


现在,我需要存储这些DrawableUpdateable 对象中的Dictionary,存储Drawable 的ID 作为键和一个包含DrawableUpdateable 对象的复杂对象和他们的远程混凝土IData

class DataHolder<T, T1> where T:Drawable, IUpdateable<T1> where T1:IData
{
    public T Entity{ get; set;}
    public IData Data{ get; set;}

    public DataHolder(T entity, IData data)
    {
        Entity = entity;
        Data = data;
    }
}

例如,假设我目前有以下实体:

var p1 = new Player();
var p1Data = new PlayerData();
var e1 = new Enemy();
var e1Data = new EnemyData();

var playerData = new DataHolder<Player, PlayerData>(p1, p1Data);
var enemyData = new DataHolder<Enemy, EnemyData>(e1, e1Data);

我现在需要一个 Dictionary 来保存实体的 ID 作为键(p1.IDe1.ID)及其 DataHolderplayerDataenemyData)及其值。

类似于以下内容(下面的代码只是显示了我想要做的事情,因此它不会编译):

 Dictionary<int, DataHolder> list = new Dictionary<int, DataHolder>();
 list.Add(p1.ID, playerData);
 list.Add(e1.ID, enemyData);

如何构建这样的字典?

[更新]

关于使用,我需要能够做到以下几点:

 foreach (var value in list.Values)
 {
     var entity = value.Entity;
     entity.Update(value.Data);
 }

我也尝试过将DataHolder的设计改成如下:

class DataHolder<T> where T:Drawable, IUpdateable<IData>
{
    public T Entity{ get; set;}
    public IData Data{ get; set;}

    public DataHolder(T entity, IData data)
    {
        Entity = entity;
        Data = data;
    }
}

然后我尝试了类似以下的方法:

var playerData = new DataHolder<Player>(p1, p1Data);  //error

但这会引发编译时错误:

“玩家”类型必须是可转换的 到 'IUpdateable' 按顺序 将其用作参数 'T' 泛型类'DataHolder'

这是出于什么原因抛出的? Player 实现 IUpdateable&lt;PlayerData&gt;PlayerData 实现 IData。这是方差的问题吗?有什么办法可以解决吗?

【问题讨论】:

  • 通过查看此处的答案,我认为您可能只用几行文本而无需或很少的代码就可以提出这个问题。基本上你要问的是如何构造一个映射到泛型类型的字典,然后能够将该泛型类型的不同实现放入同一个字典中。对吗?
  • 是的,但我想让我的意图非常清楚,以免被误解,这会导致在问题中添加更新,以澄清我真正想要的 :-)

标签: c# generics collections interface covariance


【解决方案1】:

请仔细阅读所有代码。我想这会让你得到你想要的。 首先你需要一个IDrawable 接口

interface IDrawable { int ID { get; set; } }

连同显而易见的class Drawable : IDrawable { .. } 那么你需要一个IEntity 接口

interface IEntity : IDrawable, IUpdatetable<IData>  {     }

与实现一起

class Enemy : Drawable, IEntity
{
    public Enemy(int id) : base(id) { }
    public void Update(EnemyData data)
    { ... }
    void IUpdatetable<IData>.Update(IData data)
    {
        Update(data as EnemyData);
    }
}
class Player : Drawable, IEntity
{
    public Player(int id) : base(id) { }
    public void Update(PlayerData data)
    { ... }
    void IUpdatetable<IData>.Update(IData data)
    {
        Update(data as PlayerData);
    }
}

那你需要把DataHolder通用化

interface IDataHolder
{
    IEntity Entity { get; set;  }
    IData Data { get;  set;  }
}
class DataHolder<T,T1> : IDataHolder
    where T : class, IEntity
    where T1 : class, IData
{
    T entity;
    T1 data;
    public DataHolder(T entity, T1 data)
    {
        this.entity = entity;
        this.data = data;
    }
    public T Entity { get { return entity; } set { entity = value; } }
    public T1 Data { get { return data; } set { data = value; } }
    IEntity IDataHolder.Entity
    {
        get { return entity; }
        set { entity = value as T; }
    }
    IData IDataHolder.Data
    {
        get { return data; }
        set { data = value as T1; }
    }
}

最后使用 KeyedCollection 来保存所有内容并公开 keys 属性

public class DataBag : KeyedCollection<int, IDataHolder>
{
    protected override int GetKeyForItem(IDataHolder item)
    {
        return item.Entity.ID;
    }
    public ICollection<int> Keys
    {
        get { return Dictionary.Keys; }
    }
}

这里是测试代码:

    public void TestCode()
    {
        Player p1 = new Player(100);
        Enemy e1 = new Enemy(1);

        PlayerData p1data = new PlayerData(11, "joe");
        EnemyData e1data = new EnemyData(12, 1000);

        DataHolder<Player, PlayerData> bag1 
            = new DataHolder<Player, PlayerData>(p1, p1data);
        DataHolder<Enemy, EnemyData> bag2 
            = new DataHolder<Enemy, EnemyData>(e1, e1data);

        Dictionary<int, IDataHolder> list = new Dictionary<int, IDataHolder>();
        list.Add(p1.ID, bag1);
        list.Add(e1.ID, bag2);


        foreach (int id in list.Keys )
        {
            IDataHolder item = list[id];
            // you can do this here: 
            // item.Entity.Update(item.Data);
            // or get the type specific version below:
            if (item.Entity is Player)
            {
                Player player = item.Entity as Player;
                PlayerData pdata = item.Data as PlayerData;
                player.Update(pdata);
                Console.WriteLine("ID={0} PlayerName={1} DataId={2}", 
                    player.ID, pdata.Name, pdata.ID);
            }
            else if (item.Entity is Enemy)
            {
                Enemy enemy = item.Entity as Enemy;
                EnemyData edata = item.Data as EnemyData;
                enemy.Update(edata);
                Console.WriteLine("ID={0} EnemyDamage={1} DataId={2}", 
                    enemy.ID, edata.Damage, edata.ID);
            }
        }
    }

它会微笑着编译并产生输出

ID=100 PlayerName=joe DataId=11

ID=1 EnemyDamage=1000 DataId=12

【讨论】:

  • 如果有什么能让它更通用并称它为 IIdentifiable 并且只有一个,为什么你会想要两个相同的接口 IData 和 IDrawable。另外,既然你有 IEntity 和 IData,那么此时 DataHolder 真的需要通用吗?
  • 代码被截断。尽管 IData 和 IDrawable 正确地实现了一个 ID,但设计意图(正如我从原始帖子中所理解的那样)也将包括许多其他成员。如果我将这些通用化,那么 Player 的 ID 和 PlayerData 的 ID 将必须是相同的数字(这是发帖人不想要的)。 DataHolder 可能不是通用的,但原始帖子希望在字典集合 list 中显示不同的类型,因此使用 bag1 = DataHolder&lt;Player,PlayerData&gt;bag1.Data.Name 返回值而不进行任何转换。
  • No Player.ID 和 PlayerData.ID 不会是相同的数字,因为两个类继承了相同的基类或接口并不意味着它们具有相同的数据。只有将 PlayerData.ID 设置为 Player.ID 才会获得相同的 ID。假设 ID 的类型相同,但数据不同。
【解决方案2】:

我认为您可能需要先创建一个新类。

class DrawableUpdatable<T> : Drawable, IUpdatable<T> {
    public void Update() { base.Update(); }
}

让您的 PlayerEnemy 派生自它,您的 DataHolder 将其用作第一个通用约束。这将允许您定义一个新的DataHolder&lt;DrawableUpdatable&lt;IData&gt;, IData&gt;。否则您将无法创建一个,因为EnemyPlayer 都没有派生单个对象。 (你不能说DataHolder&lt;Drawable **and** IUpdatable&lt;IData&gt;, IData&gt;

我认为无论如何,您的设计一开始就过于复杂 - 它会吓跑任何前来查看和维护它的新开发人员。我会考虑在用更复杂的代码修复它之前对其进行修改。

【讨论】:

    【解决方案3】:

    按照你写这个的方式,没有理由不这样声明你的字典:

    Dictionary<int, object> list = new Dictionary<int, object>();
    

    DataHolder 除了构造函数外没有公共方法/属性,所以在实例构造完成后它是无用的类型。还不如object

    如果您确实打算对它们进行公共操作,并且它们的参数和返回值不需要模板值,那么您可以从DataHolder 中提取一个非模板接口并将其设为字典的值类型。

    【讨论】:

      【解决方案4】:

      让您的DataHolder 类实现或继承非泛型类或接口,然后制作该(非泛型)类型的字典。

      【讨论】:

      • 同意。为什么不让您的 DataHolder 类实现 IData ,其中 DataHolder.ID 的返回值只是 DataHolder.Data.ID 那么您不需要 Data 对象也可以将 DataHolder 添加到字典中。您的 Dictionary 将变为 Dictionary 和 Your list.Add(p1.ID, playerData);会变成 list.Add(playerData.ID, playerData);
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-08-18
      • 2012-09-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-28
      相关资源
      最近更新 更多