这是我工作中遇到的一个bug,困扰了我一个下午。
但是我会用一个好理解的例子来说明。
应该在所有面向对象编程语言中是通用的,这是关于”克隆一个实体“的问题。
问题
首先,我手上有两个类,一个是Person
,另一个是Car
。
Person有一个属性叫做car
,创建新Person时,要传入Car
的实例。
1public static void test1()
2{
3 Car car = new Car("BMW", "Black", 100000);//brand , color, price
4 Person person = new Person("John", 20, car);
5 System.Console.WriteLine(person.ToString());
6 //Name: John, Age: 20, Car: Brand: BMW, Color: Black, Price: 100000
7 car.price = 200000;// if i change the price of the car
8 System.Console.WriteLine(person.ToString());
9 //Name: John, Age: 20, Car: Brand: BMW, Color: Black, Price: 200000
10 System.Console.WriteLine("===========");
11}
创建了一个价格为100000的
car
然后创建了一个
person
,传入car
在外部改变了
car
的价格已经创建的
person.car.price
的价格也改变了!
person
在创建的时候(假设他这时购买了这辆车),他的car
的价格就是100000,不应该因为car
涨价了而改变。
或者想象一下,我又修改了一下car的属性,比如说car.color = "red"
,那么person.car.color
也会变成red
,我未经同意给人家车子换了个喷漆。
函数传递参数
这涉及到一个原理:
在函数传递参数的时候,如果用的是基础类型,比如int
,string
,那么传递的是值,如果是引用类型,比如Car
class,那么传递的是引用。
这也是为什么上面的例子中,当我改变了外面car
的价格,已经创建的person.car.price
的价格也会改变。(它引用了内存地址中同一个位置的car
)
在这里例子中,我们只需要不再动car就可以了
但是实际情况是:这可能是循环中的一部分,那个car在下一次循环中可能还会被利用。从而导致car不可避免的被修改。
解决方案
其实解决方案也很简单,就是弄一个Car副本。
有两种方式可以按照需求使用 虽然我的问题中两种办法都用不了,不过我也另辟蹊径解决了)
创建一个新的Car
他的意识其实就是所谓的“深拷贝”,就是把一个对象的所有属性都拷贝一份,
传入的参数从car
变成了new Car(car.brand, car.color, car.price)
。
1private static void test2()
2{
3 Car car = new Car("BMW", "Black", 100000);//brand , color, price
4 // if i want to copy the car(this is not a good way)
5 Person person = new Person("John", 20, new Car(car.brand, car.color, car.price));
6 System.Console.WriteLine(person.ToString());
7 //Name: John, Age: 20, Car: Brand: BMW, Color: Black, Price: 100000
8 car.price = 200000;// if i change the price of the car
9 System.Console.WriteLine(person.ToString());
10 //Name: John, Age: 20, Car: Brand: BMW, Color: Black, Price: 100000
11 System.Console.WriteLine("===========");
12}
这个办法挺好的,但是有一个大大的问题,如果
Car
类中有很多属性,写起来太复杂了。❗️甚至有可能这个
Car
类是别人提供的库,还是个套娃,他里面层级深的可怕,还不给new。
使用MemberwiseClone
方法
- 在c#中,
MemberwiseClone
方法 - 在py中,
copy
函数 - 在java中,
clone
方法
MemberwiseClone
这个方法是Object
类中的一个方法,他的就是所谓的“浅拷贝”,就是把一个对象的所有属性都拷贝一份,但是如果属性是引用类型,那么只是拷贝了引用,而不是拷贝了引用的值。
MemberwiseClone
是私有属性,为了调用它,需要在car类中创建一个Clone方法
Clone方法(在Car.cs中):
1internal Car Clone()
2{
3 return (Car)this.MemberwiseClone();
4}
这回传入的参数从car
变成了car.Clone()
。
1private static void test3()
2{
3 Car car = new Car("BMW", "Black", 100000);//brand , color, price
4 // "the better clone way"you will need to implement Clone() method,go see the colne method in Car.cs
5 Person person = new Person("John", 20, car.Clone());
6 System.Console.WriteLine(person.ToString());
7 //Name: John, Age: 20, Car: Brand: BMW, Color: Black, Price: 100000
8 car.price = 200000;// if i change the price of the car
9 System.Console.WriteLine(person.ToString());
10 //Name: John, Age: 20, Car: Brand: BMW, Color: Black, Price: 200000
11 System.Console.WriteLine("===========");
12}
另辟蹊径–我的办法
我是用的是第三方的Docx库,他的Paragraph
类就是我要复制的对象。
- 他没有提供Copy方法
- new Paragraph()也不行,会有很多属性是null(并且参数一大堆)
- Paragraph对象是和文档绑定的,我需要一个Paragraph对象,总不能去复制下整个文档。
⭐️我创建了一个Clone_p
函数,他会专门返回一个Paragraph对象
传入的确实是引用,但是使用了专门copy函数后,就不会不小心修改到他了,在内存中也是独立的一个位置(docx初始化完毕之后,所有paragraph都会读到内存中放置)完美解决问题!
1public Paragraph Clone_p(Paragraph p) {
2 return find_p_by_pid(Get_pid(p));
3}
4
5public Paragraph find_p_by_pid(string value) => Paragraphs.Where(p => p.Xml.FirstAttribute.ToString()==value).ToList()[0];
6
7public static string Get_pid(Paragraph p) {
8 return p.Xml.FirstAttribute.ToString();
9}