请解释为什么列表<A>投射到 List<A.super>在 Cpp、Java 和 C# 中允许或不允许

Please explain why List<A> cast to List<A.super> allowed or not allowed in Cpp, Java and C#

本文关键字:lt gt Java Cpp 不允许 List 列表 为什么 解释 super      更新时间:2023-10-16

我发现:Cpp, Java和c#在从List of A转换到List of A.super时有不同的控制策略。我知道这三种语言有不同的实现泛型的方法。Cpp使用STL, Java使用擦除,CLR的实现较好。然而,我不知道为什么有些类型是允许的,而有些类型是不允许的。例如:

1. cpp

class A
{
protected:
    const char*_v;
public:
    A(const char* v)
    {
        _v = v;
    }
public:
    void getValue()
    {
        std::cout << "A" << _v << std::endl;
    }
};
class B : public A
{
public:
    B(const char* v) :A(v)
    {
    }
public:
    void getValue()
    {
        std::cout << "B" << _v << std::endl;
    }
};
int _tmain(int argc, _TCHAR* argv[])
{
    std::list<B> b;
    B b01("01");
    B b02("02");
    b.push_back(b01);
    b.push_back(b02);
    //no
    //std::list<A> a = static_cast <std::list<A> &>b;
    //no
    //std::list<A> a = dynamic_cast <std::list <A> & >b;
    //yes
    std::list<A> a = reinterpret_cast <std::list <A> &> (b);
    a.front().getValue();
    return 0;
}
java

2.

public static void testJava()
{
    List<String> s =  new ArrayList<String>();
    s.add("01");
    s.add("02");
    //no
    //List<Object> o = (List<Object>)s;
    //yes
    //List<Object> o = (List<Object>)(Object)s;
    //yes
    List<Object> o = (List<Object>)(List)s;
    System.out.println(o.get(0));
}
c# 3.

    private static void testCS()
    {
        List<String> s = new List<String>()
        {
            "01",
            "02"
        };
        //no
        //List<Object> o = (List<Object>)s;
        //no
        //List<Object> o = (List<Object>)(Object)s;
        //yes, but I think this is a new List
        List<Object> o = s.OfType<Object>().ToList();
        Console.WriteLine(o[0]);
    }
你能给我解释一下吗?

谢谢!

这里发生的事情是c#和Java阻止你犯严重的错误,但c++允许你这样做(因为语言的设计方式)。

下面是一些c++示例代码,演示了可能出现的错误。它与您的示例代码非常相似,只是做了一个小小的调整:在将list<B>转换为list<A>之后,它将类型为A的元素推入列表。然后,它尝试通过现有的list<B>访问该元素,并产生可预测的结果(即它在您的脸上爆炸):

#include "stdafx.h"
class A
{
protected:
    const char*_v;
public:
    A(const char* v)
    {
        _v = v;
    }
public:
    void getValue()
    {
        std::cout << "A" << _v << std::endl;
    }
};
class B : public A
{
public:
    B(const char* v) :A(v)
    {
    }
public:
    void getValue()
    {
        std::cout << "B" << _v << std::endl;
    }
    void thisIsOnlyInB()
    {
        std::cout << "Only in B" << _v << std::endl;
    }
};
int main()
{
    std::list<B> b;
    B b01("01");
    b.push_back(b01);
    std::list<A> a = reinterpret_cast <std::list <A> &> (b);
    A a01("01");
    a.push_back(a01);
    b.front().thisIsOnlyInB(); // OK - item is really a B.
    b.pop_front();
    b.front().thisIsOnlyInB(); // Oh dear - item is really an A, so this explodes.
    return 0;
}

这类问题在设计c#和Java的时候是很容易理解的,语言设计者决定通过发出编译器错误来解决它。

c++一直有一个"信任程序员"的哲学,所以它允许你进行强制转换——这取决于程序员来确保它不被滥用。(如果c++被发明得晚一些,它在这方面的设计是否会有所不同,这是一个值得讨论的问题。一些设计决策可能是当时编译器技术状态的结果。

因为泛型/模板化的List类型本质上是不变的,而您的意图是将其用作协变类型。

你可以使用类型B的任何对象,因为类型B是A的子类型,因为这种关系,你(错误地)期望任何泛型类型也保持这种关系。因此,在List<T>的例子中,您期望可以在List<A>需要的地方使用任何List<B>,但这是不可能的,因为您无法保证程序的正确性,因为突然List<B>可以被A更改。

c++、Java和c#有协方差、逆变("期望"的逆)或不变性的概念。

如果您从List接口中删除Add方法,那么您可能能够构建协变类型,因为现在该类型没有可能破坏List类型正确性的方法,因此在B对象列表中不情愿地出现c类型的cousin对象,但是没有Add的List是没有意义的。

区别基本上在于模板/泛型的作用。

虽然你的例子从c#似乎不做强制转换,但创建一个新的列表。Java我不确定,但它有一些运行时检查,c++不生成由于性能开销。

您还需要考虑的是c++模板实际上是生成代码的。因此,当您实例化std::list<typeA>时,将生成一个列表类,这是一个完全不同且不相关的类,与另一个类作为模板参数的列表(即使是基类),因此它们不能相互转换。