【转】c# 协变与抗变

转自:

协变和抗变


一.定义

在说定义之前,先看一个简单的例子:
    public class Shape
    {
    }
 
    public class Rectange : Shape
    {
    }
上面定义了两个简单的类,一个是图形类,一个是矩形类;它们之间有简单的继承关系。接下来是常见的一种正确写法:
Shape shape= new Rectange();
就是说“子类引用可以直接转化成父类引用”,或者说Rectange类和Shape类之间存在一种安全的隐式转换。
那问题就来了,既然Rectange类和Shape类之间存在一种安全的隐式转换,那数组Rectange[]和Shape[]之间是否也存在这种安全的隐式转换呢?
这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。也就是上面例子所满足的写法。
然后看下面这种写法:
Shape[] shapes=new Rectange[3];
编译通过,这说明Rectange[]和Shape[]之间存在安全的隐式转换。
像这种与原始类型转换方向相同的可变性就称作协变(covariant)


接下来试试这样写:
Rectange[] rectanges = new Shape[3];
发现编译不通过,即数组所对应的单一元素的父类引用不可以安全的转化为子类引用。数组也就自然不能依赖这种可变性,达到协变的目的。
所以与协变中子类引用转化为父类引用相反,将父类引用转化为子类引用的就称之为抗变。
即:一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫抗变!
 

当然可变性远远不只是针对映射到数组的能力,也有映射其它集合的能力如List<T>.
到这里,很多人就会问了,说了这么多,那到底这个协变或者抗变有什么实际利用价值呢?
其价值就在于,在.net 4.0之前可以这么写:
            Shape shape = new Rectange();
但是却不能这么写:
            IEnumerable<Shape> shapes = new List<Rectange>();
4.0之后,可以允许按上面的写法了,因为泛型接口IEnumerable<T>被声明成如下:
public interface IEnumerable<out T> : IEnumerable

上面提到了,数组不支持抗变。在.Net 4.0之后,支持协变和抗变的有两种类型:泛型接口和泛型委托。

 
二.泛型接口中的协变和抗变

接下来定义一个泛型接口:
public interface ICovariant<T>
{
}
并且让上面的两个类各自继承一下该接口:
    public class Shape : ICovariant<Shape>
    {
    }
 
    public class Rectange : Shape,ICovariant<Rectange>
    {
    }
编写测试代码:
        static void Main(string[] args)
        {
            ICovariant<Shape> ishape = new Shape();
            ICovariant<Rectange> irect = new Rectange();
 
            ishape = irect;
        }
编译并不能通过,原因是无法将ICovariant<Rectange>隐式转化为ICovariant<Shape>!
再将接口修改为:
    public interface ICovariant<out T>
    {
    }
编译顺利通过。这里我为泛型接口的类型参数增加了一个修饰符out,它表示这个泛型接口支持对类型T的协变。
即:如果一个泛型接口IFoo<T>,IFoo<TSub>可以转换为IFoo<TParent>的话,我们称这个过程为协变,而且说“这个泛型接口支持对T的协变”。

那我如果反过来呢,考虑如下代码:
        static void Main(string[] args)
        {
            ICovariant<Shape> ishape = new Shape();
            ICovariant<Rectange> irect = new Rectange();
 
            irect = ishape;
           // ishape =irect;
        }
发现编译又不通过了, 原因是无法将 ICovariant<Shape> 隐式转化为 ICovariant<Rectange> !
将接口修改为:
    public interface ICovariant<in T>
    {
    }
编译顺利通过。这里我将泛型接口的类型参数T修饰符修改成in,它表示这个泛型接口支持对类型参数T的抗变。
即:如果一个泛型接口IFoo<T>,IFoo<TParent>可以转换为IFoo<TSub>的话,我们称这个过程为抗变(contravariant),而且说“这个泛型接口支持对T的抗变”!

泛型接口并不单单只有一个参数,所以我们不能简单地说一个接口支持协变还是抗变,只能说一个接口对某个具体的类型参数支持协变或抗变,如ICovariant<out T1,in T2>说明该接口对类型参数T1支持协变,对T2支持抗变。
举个例子就是:ICovariant<Rectange,Shape>能够转化成ICovariant<Shape,Rectange>,这里既有协变也有抗变。

以上都是接口并没有属性或方法的情形,接下来给接口添加一些方法:
    //这时候,无论如何修饰T,都不能编译通过
    public interface ICovariant<out T>
    {
        T Method1();
        void Method2(T param);
    }
发现无论用out还是in修饰T参数,根本编译不通过。
原因是,我把仅有的一个类型参数T既用作函数的返回值类型,又用作函数的参数类型。
所以:
1)当我用out修饰时,即允许接口对类型参数T协变,也就是满足从ICovariant<Rectange>到ICovariant<Shape>转换,Method1返回值Rectange到Shape转换没有任何问题:
            ICovariant<Shape> ishape = new Shape();
            ICovariant<Rectange> irect = new Rectange();
 
            ishape = irect;
            Shape shape = ishape.Method1();
但是对于把T作为参数类型的方法Method2(Rectange)会去替换Method2(Shape):
            ICovariant<Shape> ishape = new Shape();
            ICovariant<Rectange> irect = new Rectange();
 
            ishape = irect;
            ishape.Method2(new Shape());
即如果执行最后一行代码,会发现参数中,Shape类型并不能安全转化成Rectange类型,因为Method2(Shape)实际上已经被替换成
Method2(Rectange) !

2)同样,当我用in修饰时, 即允许接口对类型参数T抗变,也就是满足从ICovariant<Shape>到ICovariant<Rectange>转换:
             ICovariant<Shape> ishape = new Shape();
            ICovariant<Rectange> irect = new Rectange();
 
            //ishape = irect;
            irect = ishape;
            irect.Method2(new Rectange());
Method2(Shape)会去替换Method2(Rectange),所以上面的最后一句代码无论以Rectange类型还是Shape类型为参数都没有任何问题;
但是Method1返回的将是Shape类型:
             ICovariant<Shape> ishape = new Shape();
            ICovariant<Rectange> irect = new Rectange();
 
            //ishape = irect;
            irect = ishape;
            Rectange rect = irect.Method1();
执行最后一句代码,同样将会是不安全的!

综上:在没有额外机制的限制下,接口进行协变或抗变都是类型不安全的。.NET 4.0有了改进,它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围,这个额外的描述即in,out修饰符,它们俩的用法如下:
如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰。

所以,需要将上面的接口拆成两个接口即可:
    public interface ICovariant<out T>
    {
        T Method1();
 
    }
 
    public interface IContravariant<in T>
    {
        void Method2(T param);
    }

.net中很多接口都仅将参数用于函数返回类型或函数参数类型,如:
public interface IComparable<in T>
public interface IEnumerable<out T> : IEnumerable

几个重要的注意点:

1.仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2.值类型不参与协变或抗变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3.声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。

接下来将接口代码改成:
    public interface ICovariant<out T>
    {
        T Method1();
        void Method3(IContravariant<T> param);
    }
 
    public interface IContravariant<in T>
    {
        void Method2(T param);
    }
同样是可以编译通过的.
我们需要费一些周折来理解这个问题。现在我们考虑ICovariant<Rectange>,它应该能够协变成ICovariant<Shape>,因为Rectange是Shape的子类。因此Method3(Rectange)也就协变成了Method3(Shape)。当我们调用这个协变,Method3(Shape)必须能够安全变成Method3(Rectange)才能满足原函数的需要(具体原因上面已经示例过了)。这里对Method3的参数类型要求是Shape能够抗变成Rectange!也就是说,如果一个接口需要对类型参数T协变,那么这个接口所有方法的参数类型必须支持对类型参数T的抗变(如果T有作为某些方法的参数类型)。
同理我们也可以看出,如果接口要支持对T抗变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-抗变互换原则。所以,我们并不能简单地说out参数只能用于方法返回类型参数,它确实只能直接用于声明返回值类型,但是只要一个支持抗变的类型协助,out类型参数就也可以用于参数类型!(即上面的例子),换句话说,in除了直接声明方法参数类型支持抗变之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T抗变的类型作为方法参数类型也是不允许的。

既然方法类型参数协变和抗变有上面的互换影响。那么方法的返回值类型会不会有同样的问题呢?
将接口修改为:
    public interface IContravariant<in T>
    {
 
    }
    public interface ICovariant<out T>
    {
 
    }
 
    public interface ITest<out T1, in T2>
    {
        ICovariant<T1> test1();
        IContravariant<T2> test2();
    }

我们看到和刚刚正好相反,如果一个接口需要对类型参数T进行协变或抗变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或抗变(如果有某些方法的返回值是T类型)。这就是方法返回值的协变-抗变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以抗变的类型作为桥梁即可。


三.泛型委托中的协变和抗变

泛型委托的协变抗变,与泛型接口协变抗变类似。继续延用Shape,Rectange类作为示例:
新建一个简单的泛型接口:
        public delegate void MyDelegate1<T>();
测试代码:
            MyDelegate1<Shape> shape1 = new MyDelegate1<Shape>(MethodForParent1);
            MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
            shape1 = rect1;
其中两个方法为:
        public static void MethodForParent1() 
        {
            Console.WriteLine("Test1");
        }
        public static void MethodForChild1()
        {
            Console.WriteLine("Test2");
        }
编译并不能通过,因为无法将MyDelegate1<Rectange>隐式转化为MyDelegate1<Shape>,接下来我将接口修改为支持对类型参数T协变,即加out修饰符:
        public delegate void MyDelegate1<out T>();
编译顺利用过。
同样,如果反过来,对类型参数T进行抗变:
            MyDelegate1<Shape> shape1 = new MyDelegate1<Shape>(MethodForParent1);
            MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
            //shape1 = rect1;
            rect1 = shape1;
只需将修饰符改为in即可:
        public delegate void MyDelegate1<in T>();

考虑第二个委托:
        public delegate T MyDelegate2<out T>();
测试代码:
            MyDelegate2<Shape> shape2 = new MyDelegate2<Shape>(MethodForParent2);
            MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
            shape2 = rect2;
其中两个方法为:
        public static Shape MethodForParent2()
        {
            return new Shape();
        }
        public static Rectange MethodForChild2()
        {
            return new Rectange();
        }
该委托对类型参数T进行协变没有任何问题,编译通过;如果我要对T进行抗变呢?是否只要将修饰符改成in就OK了?
测试如下:
        public delegate T MyDelegate2<in T>();
            MyDelegate2<Shape> shape2 = new MyDelegate2<Shape>(MethodForParent2);
            MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
            //shape2 = rect2;
            rect2 = shape2;
错误如下:
变体无效: 类型参数“T”必须为对于“MyDelegate2<T>.Invoke()”有效的 协变式。“T”为 逆变。
意思就是:这里的类型参数T已经被声明成抗变,如果上面的最后一句有效,那么以后rect2()执行结果返回的将是一个Shape类型的实例,
如果再出现这种代码:
            Rectange rectange = rect2();
那么这将是一个从Shape类到Rectange类的不安全的类型转换!所以如果类型参数T抗变,并且要用于方法返回类型,那么方法的返回类型也必须支持抗变。即上面所说的方法返回类型协变-抗变一致原则。
那么如何对上面的返回类型进行抗变呢?很简单,只要借助一个支持抗变的泛型委托作为方法返回类型即可:
        public delegate Contra<T> MyDelegate2<in T>();
        public delegate void Contra<in T>();
具体的方法也需要对应着修改一下:
        public static Contra<Shape> MethodForParent3()
        {
            return new Contra<Shape>(MethodForParent1);
        }
        public static Contra<Rectange> MethodForChild3()
        {
            return new Contra<Rectange>(MethodForChild1);
        }
测试代码:
            MyDelegate2<Shape> shape2 = new MyDelegate2<Shape>(MethodForParent3);
            MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild3);
            rect2 = shape2;
编译通过。

接下来考虑第三个委托:
        public delegate T MyDelegate3<T>(T param);
首先,对类型参数T进行协变:
        public delegate T MyDelegate3<out T>(T param);
对应的方法及测试代码:
        public static Shape MethodForParent4(Shape param)
        {
            return new Shape();
        }
        public static Rectange MethodForChild4(Rectange param)
        {
            return new Rectange();
        }
            MyDelegate3<Shape> shape3 = new MyDelegate3<Shape>(MethodForParent4);
            MyDelegate3<Rectange> rect3 = new MyDelegate3<Rectange>(MethodForChild4);
            shape3 = rect3;
和泛型接口类似,这里的委托类型参数T被同时用作方法返回类型和方法参数类型,不管修饰符改成in或out,编译都无法通过。所以如果用out修饰T,那么方法参数param的参数类型T就需借助一样东西来转换一下:一个对类型参数T能抗变的泛型委托。
即:
        public delegate T MyDelegate3<out T>(Contra<T> param);
两个方法也需对应着修改:
        public static Shape MethodForParent4(Contra<Shape> param)
        {
            return new Shape();
        }
        public static Rectange MethodForChild4(Contra<Rectange> param)
        {
            return new Rectange();
        }
这就是上面所说的方法参数的协变-抗变互换原则
同理,如果对该委托类型参数T进行抗变,那么根据方法返回类型协变-抗变一致原则,方法返回参数也是要借助一个对类型参数能抗变的泛型委托:
        public delegate Contra<T> MyDelegate3<in T>(T param);
两个方法也需对应着修改为:
        public static Contra<Shape> MethodForParent4(Shape param)
        {
            return new Contra<Shape>(MethodForParent1);
        }
        public static Contra<Rectange> MethodForChild4(Rectange param)
        {
            return new Contra<Rectange>(MethodForChild1);
        }
推广到一般的泛型委托:
        public delegate T1 MyDelegate4<T1,T2,T3>(T2 param1,T3 param2);
可能三个参数T1,T2,T3会有各自的抗变和协变,如:
        public delegate T1 MyDelegate4<out T1,in T2,in T3>(T2 param1,T3 param2);
这是一种最理想的情况,T1支持协变,用于方法返回值;T2,T3支持抗变,用于方法参数。
但是如果变成:
        public delegate T1 MyDelegate4<in T1,out T2,in T3>(T2 param1,T3 param2);
那么对应的T1,T2类型参数就会出问题,原因上面都已经分析过了。于是就需要修改T1对应的方法返回类型,T2对应的方法参数类型,如何修改?只要根据上面提到的:
1)方法返回类型的协变-抗变一致原则;
2)方法参数类型的协变-抗变互换原则!

对应本篇的例子,就可以修改成:
        public delegate Contra<T1> MyDelegate4<in T1, out T2, in T3>(Contra<T2> param1, T3 param2);

以上,协变和抗变记录到此。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438548.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

逆变与协变

一、遇到问题 这是基于.net3.5开发&#xff0c;实际工作中遇到一个问题。假设我们有一个 Base 类&#xff0c;一个 Derived 类&#xff0c;Derived 继承了 Base。如下&#xff1a; 1 2 3 4 5 6 7 8 class Base { } class Derived : Base { }   当我用IEnumerable<Bas…

php新手用7.2,总结PHP升级7.2之后需要注意的事情

最近升级了PHP版本&#xff0c;从7.1升级到7.2&#xff0c;升级前版本&#xff1a;PHP 7.1.14 (cli) (built: Feb 2 2018 08:42:59) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies with Zend OPcache v7.1.14…

第一节: 结合EF的本地缓存属性来介绍【EF增删改操作】的几种形式

一. 背景 说起EF的增删改操作&#xff0c;相信很多人都会说&#xff0c;有两种方式&#xff1a;① 通过方法操作 和 ② 通过状态控制。 相信你在使用EF进行删除或修改操作的时候&#xff0c;可能会遇到以下错误&#xff1a;“ The object cannot be deleted because it was n…

win7和mysql乱码,win7 mysql中文乱码怎么处理

方法/步骤【第一步】在mysql dos命令窗口中输入下面这段命令SHOW VARIABLES LIKE character_set_%; //注 用于显示【mysql 的编码设置】2显示了之后 显示你的mysql编码设置和我的不同之处改掉就OK了【你直接复制下面的命令 粘贴到dos命令窗口中就OK了】【注 我这个改法 只有新添…

第十六节:语法总结(3)(C#6.0和C#7.0新语法)

一. C# 6.0 新语法 1. 自动属性初始化可以赋值 1 /// <summary>2 /// 自动属性初始化3 /// </summary>4 public class UserInfor5 {6 public string userId { get; set; } "123456";7 8 public string userName {…

php仿伊人集源码,仿伊人集wecenter()包含所有功能及官方付费插件,整站带数据...

演示地址&#xff1a;如有演示站请以演示为准&#xff0c;无演示站以截图为准,源码太多服务器有限&#xff0c;无法搭建所有源码演示站&#xff0c;请谅解&#xff01;新手购买指导&#xff1a;1.在本站注册账号 丨 2.登录已注册账号充值源码所需金币 丨 3.登录账号下载所需源码…

第十七节:易混淆的概念(静态和非静态、拆箱和装箱)

一. 静态和非静态 1. 概念介绍 ① 静态类&#xff08;被static修饰&#xff09; vs 普通类&#xff08;没有被static修饰&#xff09; ② 静态成员&#xff1a;被static修饰的成员&#xff0c;比如&#xff1a;静态方法、静态字段等 ③ 普通成员(实例成员)&#xff1a;不被st…

perl连接oracle的方法,Perl 连接Oracle 数据库

1.安装DBI和DBD::Oracle 模块。下载地址&#xff1a;搜索DBI&#xff0c;DBD::Oracle如果你的linux可以连接网络&#xff0c;执行命令&#xff1a;#wget#wget下载到当前目录。2.安装这里主要都是源码安装。解压到目录&#xff1a;DBI-1.622#tar –zxvf DBI-1.622.tar.gz安装可…

第十八节:跨域请求的解决方案和WebApi特有的处理方式

一. 简介 前言&#xff1a; 跨域问题发生在Javascript发起Ajax调用&#xff0c;其根本原因是因为浏览器对于这种请求&#xff0c;所给予的权限是较低的&#xff0c;通常只允许调用本域中的资源&#xff0c; 除非目标服务器明确地告知它允许跨域调用。假设我们页面或者应用已在 …

bit类型在oracle,奇怪的Oracle bitand函数

最近在用Oracle的bitand函数是遇到了一个奇怪的现象&#xff1a;declaren number default374123538;val number;begindbms_output.enable;val : bitand(n,16777215);dbms_output.put_line(val);end;/结果正确&#xff1a;5024786PL/SQL procedure successfully completed然后修…

ORM系列之Entity FrameWork详解

一. 谈情怀 从第一次接触开发到现在&#xff08;2018年&#xff09;&#xff0c;大约有六年时间了&#xff0c;最初阶段连接数据库&#xff0c;使用的是【SQL语句ADO.NET】&#xff0c;那时候&#xff0c;什么存储过程、什么事务 统统不理解&#xff0c;生硬的将SQL语句传入SQL…

oracle行转列 case,Oracle 行转列总结 Case When,Decode,PIVOT 三种方式

Decode 方式 SELECT a.COUNTRY AS "国家", SUM(DECODE(a.SEX, 1, a.POPULCATION)) AS "男", SUM(DECODE(a.SEX, 2, a.POPULCATION)) AS "女" FROM populations a GROUP BY a.COUNTRY -------------------------------------- Case When 方式 SEL…

第二节: 比较EF的Lambda查询和Linq查询写法的区别

简介 在前面EF的介绍中&#xff0c;曾多次提到过EF可以使用Lambda和Linq来完成对数据库的访问&#xff0c;这两种的语法的具体使用和注意事项在前面的DotNet进阶的系列章节中已经详细介绍过了&#xff0c;本次借着EF章节&#xff0c;重新调整了一下里面的规范和注意事项。 1. …

oracle 9i 只读模式,我的oracle 9i学习日志(6)--Starting Up and shutting down a Database

Starting Up a Database1、NOMOUNT这个状态下oracle server完成下列任务&#xff1a;a.读取初始化参数文件&#xff1b;b.为SGA分配内存&#xff1b;c.启动后台进程&#xff1b;d.打开alertSID.log文件和跟踪文件数据库必须被命名&#xff0c;即初始化文件或在startup命令里指定…

第三节: EF调用普通SQL语句的两类封装(ExecuteSqlCommand和SqlQuery )

一. 前言 在前面的两个章节中&#xff0c;我们分别详细介绍了EF的增删改的两种方式&#xff08;方法和状态&#xff09;和EF查询的两种方式( Lambda和Linq )&#xff0c;进行到这里&#xff0c;可以说对于EF&#xff0c;已经入门了&#xff0c;本来应该继续往下进行EF的高级属性…

连续反应matlab,MATLAB和Monte Carlo法在连续反应动力学中的应用.pdf

第42卷第4期 广 州 化 工 Vo1&#xff0e;42No&#xff0e;42014年 2月 GuangzhouChemicalIndustry Feb&#xff0e;2014MATLAB和 MonteCarlo法在连续反应动力学中的应用李 旭 &#xff0c;陈志刚(1湘南学院化学与生命科学系&#xff0c;湖南 郴州 423043&#xff1b;2中南大学…

第四节: EF调用存储过程的通用写法和DBFirst模式子类调用的特有写法

一. 背景 上一个章节&#xff0c;介绍了EF调用两类SQL语句&#xff0c;主要是借助 ExecuteSqlCommand 和 SqlQuery 两个方法来完成&#xff0c;在本章节主要是复习几类存储过程的写法和对应的EF调用这几类存储过程的写法&#xff0c;另外介绍EF的DBFirst模式的下EF存储过程的特…

oracle cogs 科目,请问R12中,Mtl_material_transaction中的 COGS Recognition记录是干啥的?

我今天抓取库存移动记录,发现Mtl_material_Transaction表中,在作完了sales Order 的ship confirm动作后产生了两笔库存移动记录,第一笔是Sales Issue的出库,quantity为负数,还有另外一笔,transaction type是 COGS Recognition,数量和SalesIssue的数量相等,但是为正数, 在Form界…

第五节: EF高级属性(一) 之 本地缓存、立即加载、延迟加载(不含导航属性)

一. 本地缓存 从这个章节开始&#xff0c;介绍一下EF的一些高级特性&#xff0c;这里介绍的首先介绍的EF的本地缓存&#xff0c;在前面的“EF增删改”章节中介绍过该特性&#xff08;SaveChanges一次性会作用于本地缓存中所有的状态的变化&#xff09;&#xff0c;在这里介绍一…

oracle卸数什么是ctrl文件,Oracle数据库的安装和卸载之道

成功安装数据库1、如果安装数据库时有两个文件&#xff0c;需要将两个文件合并后再安装将两个文件都解压后&#xff0c;1of2包含安装文件&#xff0c;2of2只包含jar包&#xff0c;需要将2of2中stage\Components下的文件夹复制到1of2的该目录下注&#xff1a;如果无此处操作&…