A design problem involving Collections, Generics and Interfaces in C#
This post contains a lot of code, but I would really appreciate it if you took the some to read and understand it...and hopefully come up with a solution
Let's assume that I am structuring a network-game where the entities need to be drawn and some of them updated from the server accordingly.
The Drawable
class is in charge of drawing the entities on-screen:
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() { }
}
The data that is received from the server implements IData
with each concrete IData
holding different properties. Let's say we have the following data that the Player
and Enemy
will receive:
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; }
}
The game entities that should be updateable from the server implement IUpdateable<T>
where T
is IData
:
interface IUpdateable<T> where T : IData
{
void Update(T data);
}
Our entities are thus Drawable
and Updateable
:
class Enemy : Drawable, IUpdateable<EnemyData>
{
public void Update(EnemyData data) { }
}
class Player : Drawable, IUpdateable<PlayerData>
{
public void Update(PlayerData data) {}
}
So that's the basic structure of the game.
Now, I need to store a Dictionary
of these Drawable
and Updateable
objects, storing the Drawable
's ID as the key and a complex object that holds the Drawable
and Updateable
objects and their remote concrete 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;
}
}
As an example, say I currently have the following entities:
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);
I now need to have a Dictionary
that holds the entities' ID as a key (p1.ID
and e1.ID
) and their DataHolder
(playerData
and enemyData
) and their value.
Something like the following (the below code just shows what I want to do and thus it doesn't not compile):
Dictionary<int, DataHolder> list = new Dictionary<int, DataHolder>();
list.Add(p1.ID, playerData);
list.Add(e1.ID, enemyData);
How do I construct such a Dictionary?
[Update]
As regards usage, I will 开发者_如何学运维then need to be able to do the following:
foreach (var value in list.Values)
{
var entity = value.Entity;
entity.Update(value.Data);
}
I have also tried to change the design of DataHolder
to the following:
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;
}
}
Then I tried something like the following:
var playerData = new DataHolder<Player>(p1, p1Data); //error
But that throws a compile-time error:
The type 'Player' must be convertible to 'IUpdateable<IData>' in order to use it as parameter 'T' in the generic class 'DataHolder<T>'
For what reason is this thrown? Player
implements IUpdateable<PlayerData>
and PlayerData
implements IData
. Is this an issue with variance? And is there any way around it ?
Make your DataHolder
class implement or inherit a non-generic class or interface, then make a dictionary of that (non-generic) type.
The way you've written this, there's no reason not to declare your dictionary thusly:
Dictionary<int, object> list = new Dictionary<int, object>();
DataHolder
has no public methods/properties except for the constructor, so it is a useless type after the instance has been constructed. It is no better than object
If you do intend to put public operations on them and their arguments and return values don't require templated values, then you can extract a non-templated interface from DataHolder
and make that the value type of the dictionary.
I think you'll probably need to create a new class to begin with.
class DrawableUpdatable<T> : Drawable, IUpdatable<T> {
public void Update() { base.Update(); }
}
Have your Player
and Enemy
derive from it, and your DataHolder use it as the first generic constraint. This will allow you to define a new DataHolder<DrawableUpdatable<IData>, IData>
. You're not going to be able to create one otherwise, because there is no single object that both Enemy
and Player
derive. (You can't say DataHolder<Drawable **and** IUpdatable<IData>, IData>
I think your design is way overcomplicated to begin with anyway - it'd frighten away any new developer who came to look at it and maintain it. I would consider revising it before fixing it with even more complex code.
Please read carefully all the code. I think this will get you what you want.
First you need a IDrawable
interface
interface IDrawable { int ID { get; set; } }
together with the obvious class Drawable : IDrawable { .. }
then you need an IEntity
interface
interface IEntity : IDrawable, IUpdatetable<IData> { }
together with the implementations
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);
}
}
Then you need to commonize the 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; }
}
}
and finally use a KeyedCollection to hold everything and expose the keys property
public class DataBag : KeyedCollection<int, IDataHolder>
{
protected override int GetKeyForItem(IDataHolder item)
{
return item.Entity.ID;
}
public ICollection<int> Keys
{
get { return Dictionary.Keys; }
}
}
here is the test code:
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);
}
}
}
and it compiles with a smile and yields the output
ID=100 PlayerName=joe DataId=11
ID=1 EnemyDamage=1000 DataId=12
精彩评论