在C#中创建一个通用的对象列表

9
作为介绍,我正在创建一个基础的四叉树引擎,用于个人学习目的。我希望这个引擎能够处理许多不同类型的形状(目前我选择使用圆和正方形),所有形状都会在窗口中移动,并在发生碰撞时执行某些操作。
以下是我迄今为止拥有的形状对象:
public class QShape {
    public int x { get; set; }
    public int y { get; set; }
    public string colour { get; set; }
}

public class QCircle : QShape {
    public int radius;
    public QCircle(int theRadius, int theX, int theY, string theColour) {
        this.radius = theRadius;
        this.x = theX;
        this.y = theY;
        this.colour = theColour;
    }
}

public class QSquare : QShape {
    public int sideLength;
    public QSquare(int theSideLength, int theX, int theY, string theColour) {
        this.sideLength = theSideLength;
        this.x = theX;
        this.y = theY;
        this.colour = theColour;
    }
}

现在我的问题是,如何在C#中创建一个通用列表(List<T> QObjectList = new List<T>();),以便我可以拥有一个列表,其中包含所有这些可能具有不同属性的各种形状(例如,QCircle具有“半径”属性,而QSquare具有“边长”属性)? 最好提供实现示例。
我只知道这个问题有一个非常明显的答案,但无论如何,我都会感激任何帮助。我正在尝试重新学习C#; 显然已经有一段时间了...

7
这段代码的意思是:定义了一个泛型列表,其中元素类型为QShape。问号代表类型参数需要在上下文中进行推断或指定。 - Ian Mercer
这样的列表是否包括从QShape继承的类?如果是,那么我是正确的;答案非常明显。 - Djentleman
@Djentleman:是的,它可以。但是除非你强制转换单个对象,否则你只能访问QShape属性。 - Cameron
我能得到答案形式的回复吗,包括一个实现的例子(即如何从List<QShape>访问QCircle和QSquare特定的属性)? - Djentleman
@Djentleman,你做不到。不确定为什么他的评论会被点赞。你明确表示要访问父类中不包含的不同属性,因此使用基类作为类型是行不通的。 - Pete
8个回答

7

你需要使用向下转型

将对象存储在具有基类的列表中

 List<QShape> shapes = new List<QShape>

如果您知道一个对象是什么,那么您可以安全地将其向上转型。例如:
if(shapes[0] is QSquare)
{
     QSquare square = (QSquare)shapes[0]
} 

你可以隐式地将对象向下转换。
QSquare square = new Square(5,0,0,"Blue");
QShape shape =  square

点击此处了解更多有关向上转型和向下转型的信息


2
如果在处理泛型集合中的项目时需要测试类型和转换,那么这意味着出了问题。 - spender
2
我相信你可以在你的例子中使用 is 关键字,它已经可用了相当长的时间。如果(myObject is MyType) - Chris Gessler
再次强调,如果您正在迭代QShape列表,则无法访问QSquareQCircle的属性/方法。通过在if语句中检查是否为QSquare并在其上进行转换,您就可以访问QSquare的属性/方法。此答案中的方法适用于检查几个类,但是如果您开始添加更多子类,我相信您会看到它很快变得难以控制。 - Pete
抱歉,我误解了 downcast 的含义。非常好的答案,非常有启发性。谢谢。 - Djentleman
@Joel 注意,is测试的是赋值兼容性而不是类型相等性,因此它们并不等价。具体来说,对于QSquare的子类,x.GetType() == typeof(QSquare)返回false,而x is QSquare返回true。 - phoog
显示剩余3条评论

3
您应该实现一个接口(Interface)。例如:
public interface IHasLength
{
    int Length;
}

然后在实现中,您可以这样做。
public class QSquare : QShape, IHasLength {
    public int sideLength;
    public QSquare(int theSideLength, int theX, int theY, string theColour) {
        this.sideLength = theSideLength;
        this.x = theX;
        this.y = theY;
        this.colour = theColour;
    }
    public int Length { get { return sideLength; } }
}
public class QCircle : QShape, IHasLength {
    public int radius;
    public QSquare(int theSideLength, int theX, int theY, string theColour) {
        this.sideLength = theSideLength;
        this.x = theX;
        this.y = theY;
        this.colour = theColour;
    }
    public int Length { get { return radius; } }
}

最终,在您的列表中:
List<IHasLength> shapesWithSomeLength = new List<IHasLength>();

现在,您的列表可以容纳任何实现了 IHasLength 接口的东西,无论是 QCircleQShape,甚至是 QDuck,只要它实现了 IHasLength 接口。

我喜欢这个答案,但在这种情况下使用接口而不是超类有什么特别的原因吗? - Djentleman
2
@Djentleman 因为你的类没有共同的属性名称。因此,如果你创建了一个 QShape 列表并想要遍历它并获取所有形状(QCircleQSquare)的长度,就无法做到。如果你实现接口,那么你就可以做到这一点。 - Pete
啊,我明白了。非常好。是时候学习接口了!谢谢 :) - Djentleman
只是一个快速的提示。我采用了这种方法,除了我将 IHasLength 更改为 ISpatialNode。这个接口是为了 x、y 定位。更有意义,意味着我可以将任何需要定位的对象添加到我的 Quadtree 中。 - Djentleman
@Djentleman 当然,你想使用任何对你的实现有意义的名称/属性。我只是选择了“Length”作为示例,因为这是你在问题中提到的(半径,边长)。我相信现在一切都很清楚;) - Pete

2
你可以将它们存储在一个List<QShape>中,但这意味着你无法访问特定于类型的属性。
通常情况下,您可以通过在基类中提供一个公共接口,并在子类中覆盖行为来解决这个问题。通过这种方式,一个公共接口可以隐藏各种行为。例如,一个Grow方法可以隐藏不同形状的物品增长的复杂性,并且可以在不明确知道其操作的形状的情况下调用。
public abstract class QShape {
    public abstract void Grow(int amt);
}
public class QSquare : QShape {
    private int sideLength;
    public override void Grow(int amt)
    {
        sideLength+=amt;
    }
}
public class QCircle : QShape {
    private int radius;
    public override void Grow(int amt)
    {
        radius+=amt;
    }
}

QShape 也应该是抽象的。另外,在你的例子中我没有看到接口的使用。 - Chris Gessler
我宽泛地使用“接口”这个术语。所有对象都有一个接口,因为它们提供方法和属性... 不仅仅是那些继承了“接口”实现的对象。 - spender
我喜欢这个,类似于Pete的答案。谢谢! - Djentleman

2

这是您想要的吗?

(这句话已经被翻译成中文)
public class QShape
{
    protected QShape() { }
    public int x { get; set; }
    public int y { get; set; }
    public string colour { get; set; }
}

public class QCircle : QShape
{
    public int radius;
    public QCircle(int theRadius, int theX, int theY, string theColour)
    {
        this.radius = theRadius;
        this.x = theX;
        this.y = theY;
        this.colour = theColour;
    }
}

public class QSquare : QShape
{
    public int sideLength;
    public QSquare(int theSideLength, int theX, int theY, string theColour)
    {
        this.sideLength = theSideLength;
        this.x = theX;
        this.y = theY;
        this.colour = theColour;
    }
}
class Program
{
    static void Main(string[] args)
    {
        List<QShape> list = new List<QShape>();
        list.Add(new QCircle(100, 50, 50, "Red"));
        list.Add(new QCircle(100, 400, 400, "Red"));
        list.Add(new QSquare(50, 300, 100, "Blue"));


        foreach (var item in list.OfType<QCircle>())
        {
            item.radius += 10;
        }

        foreach (var item in list.OfType<QSquare>())
        {
            item.sideLength += 10;
        }
    }
}

一个问题:通过在迭代中使用条件语句对不同类型执行操作,而不是像这里一样为每个形状分别迭代两次,是否更有效率? - Djentleman
@Djentleman:是的,但是......对于小列表(<10,000项),可能没有什么区别。从OP中并不清楚目标/要求是什么。 - John Alexiou

1

我感觉好像少了点什么,但是……

List<QCircle> circleObjects = new List<QCircle>();

并且

List<QSquare> squareObjects = new List<QSquare>();

将完美地工作。

编辑:

啊,我没有理解被问的是什么。

是的,由于您的QCircleQSquare类继承自QShape,因此您可以这样做。

List<QShape> shapes= new List<QShape>();

值得注意的是,如果您想访问该列表中所有QCircle的radius属性,则需要根据类型过滤列表。

1
OP想要一个包含两种类型的List<T>,因此应该使用List<QShape>。 - Inisheer
没错,那样做可以实现,但他想要一个列表来容纳任何类型的 QShapeQCircleQSquare - Pete
我应该表述得更清楚,但如果可能的话,我想要一个单一的列表。 - Djentleman

1

您可以使用Ian Mercer的注释List<QShape>

以下是如何填充它的方法:

List<QShape> shapes = new List<QShape>();
QCircle circle = new QCircle(); 

shapes.Add(circle);

拆箱操作:

QCircle circle = (QCircle) shapes[0];

如果您需要调用基类的方法,无需取消装箱,只需使用它即可。

好的回答,非常简单而且富有信息性。Joel的回答也很相似但稍微详细一些,不过还是谢谢! - Djentleman

-1

存储

您已经在类定义方面走在了正确的道路上。您需要做的是创建一个超类(在本例中为QShape)的List,它将能够容纳所有形状。

以下是如何创建它的示例:

List<QShape> objects = new List<QShape>();

objects.add(new QCircle(...));
objects.add(new QSquare(...));

访问

这里的问题是一旦所有内容都在列表中,如何区分它们。这可以通过 C# 的 getType()typeof() 方法来完成(Jon Skeet 在如何执行此操作方面有一个很好的答案)。基本上,它看起来像这样:

if(objects.get(some_position).getType() == typeof(QCircle))
  QCircle circle = objects.get(some_position);
else if(/* like above with QSquare */)
  QSquare square = objects.get(some_position);

完成此操作后,您可以像往常一样继续使用对象。但是,如果您尝试从列表中访问它们,则只能使用QShape具有的方法和变量,因为放入列表中的每个对象都将被强制转换为它。


对象.get().getType() 是什么鬼?为什么不直接缩写成 if(myObject is MyType)? - Chris Gessler
虽然你的语法可能有些错误,但我理解你的意思,谢谢! - Djentleman
@ChrisGessler 测试对象类型是否与已知类型相等并不等同于使用 is 运算符(而且99%的情况下程序员都希望使用 is 运算符的行为,因此类型相等性检查通常是一个错误)。 - phoog

-1
public Class abstract  Base<T>
{
    public abstract List<T>GetList();
}

然后执行这个操作

public class className:Base<ObjectName>
{
    public override List<T>GetList()
    {
        //do work here 
    }
}

一些解释会很有用。 - matsjoyce

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接