设计模式-值类型与引用类型、深拷贝与浅拷贝、原型模式详解

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
简介:  如果拷贝的时候共享被引用的对象就是浅拷贝,如果被引用的对象也拷贝一份出来就是深拷贝。(深拷贝就是说重新new一个对象,然后把之前的那个对象的属性值在重新赋值给这个用户)

一. 值类型和引用类型

  1. 前言
    (1). 分类

  值类型包括:布尔类型、浮点类型(float、double、decimal、byte)、字符类型(char)、整型(int、long、short等)、枚举(entum)、结构体(struct)。

  引用类型:数组、字符串(string)、类、接口、委托(delegate)。

(2).内存存储

  值类型数据存放在栈stack中, 引用类型地址存放栈stack中,数据存放在堆heap中。

  值类型变量声明后,不管是否赋值,都会在在栈中分配内存空间。引用类型声明时,只在栈中分配内存,用于存放地址,并没有在堆上分配内存空间。

  1. 对象的传递
    (1). 将值类型的变量赋值给另一个变量,会执行一次赋值,赋值变量包含的值。

(2). 将引用类型的变量赋值给另一个引用类型变量,它复制的是引用对象的内存地址,在赋值后就会多个变量指向同一个引用对象实例。

代码分享:

Console.WriteLine("------------------------下面是值类型和引用类型赋值-----------------------------");
//值类型赋值
int a = 0;
int b = a;
Console.WriteLine($"默认值: a={a},b={b}");
a = 1;
Console.WriteLine($"修改a的值后: a={a},b={b}");
b = 2;
Console.WriteLine($"修改b的值后: a={a},b={b}");

                //引用类型赋值
                Student stu1 = new Student();
                stu1.age = 20;
                Student stu2 = stu1;
                Console.WriteLine($"默认值: stu1.age={ stu1.age}, stu2.age={stu2.age}");
                stu1.age = 30;
                Console.WriteLine($"修改stu1.age的值后:stu1.age={ stu1.age}, stu2.age={stu2.age}");
                stu2.age = 40;
                Console.WriteLine($"修改stu2.age的值后: stu1.age={ stu1.age}, stu2.age={stu2.age}");

运行结果:

  1. 参数按值传递
    (1). 对于值类型(age),传递的是该值类型实例的一个副本,因此原本的值age并没有改变。

(2). 对于引用类型(Student stu),传递是变量stu的引用地址(即stu对象实例的内存地址)拷贝副本,因此他们操作都是同一个stu对象实例。

代码分享:

{
Console.WriteLine("------------------------下面是参数按值传递-----------------------------");
//值类型按值传递
int age1 = 60;
Utils.AddAge1(age1);
Console.WriteLine($"age={age1}");

                //引用类型按值传递
                Student stu2 = new Student();
                stu2.age = 100;
                Utils.ReduceAge1(stu2);
                Console.WriteLine($"age={stu2.age}");

}
public class Utils
{
public static void ReduceAge1(Student stu)
{
stu.age -= 10;
Console.WriteLine($"ReduceAge age={stu.age}");
}

    public static void AddAge1(int age)
    {
        age += 10;
        Console.WriteLine($"AddAge age ={age}");
    }

    public static void ReduceAge2(ref Student stu)
    {
        stu.age -= 10;
        Console.WriteLine($"ReduceAge  age={stu.age}");
    }

    public static void AddAge2(ref int age)
    {
        age += 10;
        Console.WriteLine($"AddAge age ={age}");
    }

}
运行结果:

  1. 参数按引用类型传递
     不管是值类型还是引用类型,可以使用ref或out关键字来实现参数的按引用传递。ref或out关键字告诉编译器,方法传递的是参数地址,而非参数本身。在按引用传递时,方法的定义和调用都必须显式的使用ref或out关键字,不可以省略,否则会引起编译错误。

代码分享:

{
Console.WriteLine("------------------------下面是参数按引用传递-----------------------------");
//值类型按值传递
int age1 = 60;
Utils.AddAge2(ref age1);
Console.WriteLine($"age={age1}");

                //引用类型按值传递
                Student stu2 = new Student();
                stu2.age = 100;
                Utils.ReduceAge2(ref stu2);
                Console.WriteLine($"age={stu2.age}");

}
public class Utils
{
public static void ReduceAge1(Student stu)
{
stu.age -= 10;
Console.WriteLine($"ReduceAge age={stu.age}");
}

    public static void AddAge1(int age)
    {
        age += 10;
        Console.WriteLine($"AddAge age ={age}");
    }

    public static void ReduceAge2(ref Student stu)
    {
        stu.age -= 10;
        Console.WriteLine($"ReduceAge  age={stu.age}");
    }

    public static void AddAge2(ref int age)
    {
        age += 10;
        Console.WriteLine($"AddAge age ={age}");
    }
}

运行结果:

更多C++后台开发技术点知识内容包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,音视频开发,Linux内核,TCP/IP,协程,DPDK多个高级知识点。

C/C++Linux服务器开发高级架构师/C++后台开发架构师​免费学习地址

【文章福利】另外还整理一些C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,免费分享有需要的可以点击领取

  1. string和其它引用类型的区别
    (1). 在string字符串,一开始s1地址指向是ypf,因为s2=s1,所以s2地址也同样指向ypf;当s1再次赋值lmr时,堆中就会开辟出数据lmr,而且ypf没有消失,没有被覆盖。s1地址就指向lmr,s2地址还是原来的ypf。

(2). 在引用类型数组上,一开始arry1和arry2的地址都指向{1,2,3},当给arry1进行数据更改时,由于是引用类型,所以在{1,2,3}上面进行更改,就会对arry2进行覆盖。

代码如下:

{
Console.WriteLine("------------------------下面是string和其它引用类型的区别-----------------------------");
//string类型的测试
string s1 = "ypf";
string s2 = s1;
Console.WriteLine($"s1={s1},s2={s2}");
//修改s1的值
s1 = "lmr";
Console.WriteLine($"s1={s1},s2={s2}");

                //其它引用类型的测试
                int[] arry1 = new int[] { 1, 2, 3 };
                int[] arry2 = arry1;
                Console.WriteLine($"默认值:arry1[0]={arry1[0]},arry1[1]={arry1[1]},arry1[2]={arry1[2]}");
                Console.WriteLine($"默认值:arry2[0]={arry2[0]},arry2[1]={arry2[1]},arry2[2]={arry2[2]}");

                arry1[2] = 0;
                Console.WriteLine($"修改后:arry1[0]={arry1[0]},arry1[1]={arry1[1]},arry1[2]={arry1[2]}");
                Console.WriteLine($"修改后:arry2[0]={arry2[0]},arry2[1]={arry2[1]},arry2[2]={arry2[2]}");

}
运行效果:

string变化图如下:

Array类型变化图如下:

  1. 拆箱和装箱
     装箱是值类型向引用类型转换时发生的,拆箱是引用类型向值类型转换时发生的。装箱是隐式的,拆箱是显式的

代码分享:

{
Console.WriteLine("------------------------下面是拆箱和装箱-----------------------------");
int a = 123;
object obj = a; //装箱(隐式)
Console.WriteLine($"装箱后的结果obj={obj}");

                int b = (int)obj; //拆箱(显式)
                Console.WriteLine($"拆箱后的结果b={b}");

}
运行效果

  1. 总结
    (1). 值类型有更好的效率,但不支持多态,适合用作存储数据的载体。而引用类型支持多态,适合用于定义程序的行为。

(2). 引用类型可以派生新的类型,而值类型不能。

二. 深拷贝和浅拷贝

  1. 浅拷贝
     创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是值类型和string类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址(string类型除外),所以修改其中一个对象,就会影响到另一个对象。

  2. 深拷贝
     将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象和原对象的修改不会相互影响.

  3. 二者区别
     最根本的区别在于是否真正获取一个对象的复制实体,而不是引用,假设B复制了A,修改A的时候,看B是否发生变化:

(1).如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值)

(2).如果B没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)

简单的来说:

 如果拷贝的时候共享被引用的对象就是浅拷贝,如果被引用的对象也拷贝一份出来就是深拷贝。(深拷贝就是说重新new一个对象,然后把之前的那个对象的属性值在重新赋值给这个用户)

  1. .Net中实现
    (1).浅拷贝通过MemberwiseClone()方法实现.

(2).深拷贝可以通过流的方式和反射的方式来实现,其中流的方式类前必须加 [Serializable], 反射的方式需要考虑的问题很多,嵌套以及各种类型, 此处提供的方法并不完善.

代码分享:

   /// <summary>
    /// 克隆方法(基于浅拷贝)
    /// 用于实现ICloneable里方法(当然你用来实现深拷贝也可以)
    /// </summary>
    /// <returns></returns>
    public object Clone()
    {
        return this.MemberwiseClone();
    }


    /// <summary>
    /// 克隆方法(基于深拷贝)
    /// 类前面必须加可序列化标志[Serializable]
    /// </summary>
    /// <returns></returns>
    public object DeepClone1()
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter bFormatter = new BinaryFormatter();
            bFormatter.Serialize(stream, this);
            stream.Seek(0, SeekOrigin.Begin);
            return bFormatter.Deserialize(stream);
        }
    }

基于反射的深拷贝:(仅供参考,不是很完善)

public class Utils
{
///
/// 基于反射的深拷贝
/// (存在问题,不是很好用)
///
///


///
public static object DeepClone(Object obj)
{
Type type = obj.GetType();
//对于没有公共无参构造函数的类型此处会报错
object returnObj = Activator.CreateInstance(type);
FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
for (int i = 0; i < fields.Length; i++)
{
FieldInfo field = fields[i];
var fieldValue = field.GetValue(obj);
///值类型,字符串,枚举类型直接把值复制,不存在浅拷贝
if (fieldValue.GetType().IsValueType || fieldValue.GetType().Equals(typeof(System.String)) || fieldValue.GetType().IsEnum)
{
field.SetValue(returnObj, fieldValue);
}
else
{
field.SetValue(returnObj, DeepClone(fieldValue));
}
}
//属性
PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
for (int i = 0; i < properties.Length; i++)
{
PropertyInfo property = properties[i];
var propertyValue = property.GetValue(obj);
if (propertyValue.GetType().IsValueType || propertyValue.GetType().Equals(typeof(System.String)) || propertyValue.GetType().IsEnum)
{
property.SetValue(returnObj, propertyValue);
}
else
{
property.SetValue(returnObj, DeepClone(propertyValue));
}
}
        return returnObj;
    }

}
  1. 经过测试得出来一个结论
    (1).对于浅拷贝:所有值类型和string这个引用类型修改其中一个对象的值,不相互影响; 除了string以外的引用类型都相互影响; 类属于引用类型,修改类中的一个属性值,被拷贝的另一个对象的属性值也会发生变化(与类中的属性值是什么类型没有关系).

(2).对于深拷贝:无论是值类型还是引用类型, 修改其中一个对象的值都不会相互影响。

代码分享:

/// <summary>
/// Video视频类
/// </summary>
[Serializable]
public class Video : ICloneable
{
    public string Id { set; get; } // 视频编号
    public string Content { set; get; } // 视频内容

    public List<int> ageList { set; get; }

    public VideoDetails vDetails { get; set; }

    /// <summary>
    /// 克隆方法(基于浅拷贝)
    /// 用于实现ICloneable里方法(当然你用来实现深拷贝也可以)
    /// </summary>
    /// <returns></returns>
    public object Clone()
    {
        return this.MemberwiseClone();
    }

    /// <summary>
    /// 克隆方法(基于深拷贝)
    /// 类前面必须加可序列化标志[Serializable]
    /// </summary>
    /// <returns></returns>
    public object DeepClone1()
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter bFormatter = new BinaryFormatter();
            bFormatter.Serialize(stream, this);
            stream.Seek(0, SeekOrigin.Begin);
            return bFormatter.Deserialize(stream);
        }
    }
}
/// <summary>
/// 视频详情类
/// </summary>
[Serializable]
public class VideoDetails
{
    public string videoUrl { get; set; }

    public int videoPic { get; set; }

    public VideoDetails(string myVideoUrl, int myVideoPic)
    {
        this.videoUrl = myVideoUrl;
        this.videoPic = myVideoPic;
    }
}

测试代码

           #region 浅拷贝
            {
                Console.WriteLine("-------------------------------下面是基于浅拷贝------------------------------------");
                Video v1 = new Video()
                {
                    Id = "001",
                    Content = "西游记",
                    ageList = new List<int>() { 000 },  //List是引用类型
                    vDetails = new VideoDetails(@"H:\DesignMode", 1111) //类也是引用类型
                };

                Video v2 = (Video)v1.Clone();
                Console.WriteLine($"v2: Id={v2.Id},Content={v2.Content},ageList[0]={ v1.ageList[0]},Url={v2.vDetails.videoUrl},videoPic={v2.vDetails.videoPic}");
                //修改v1的值,v2中的属性是否变化要分情况讨论的
                v1.Content = "水浒传";
                v1.ageList[0] = 111;
                v1.vDetails.videoUrl = @"H:\XXXXXXX";
                v1.vDetails.videoPic = 22222;
                Console.WriteLine($"v2: Id={v2.Id},Content={v2.Content},ageList[0]={ v2.ageList[0]},Url={v2.vDetails.videoUrl},videoPic={v2.vDetails.videoPic}");
            }
            #endregion

            #region 深拷贝1(流的模式)
            {
                Console.WriteLine("-------------------------------下面是基于深拷贝1------------------------------------");
                Video v1 = new Video()
                {
                    Id = "001",
                    Content = "西游记",
                    ageList = new List<int>() { 000 },
                    vDetails = new VideoDetails(@"H:\DesignMode", 1111)
                };

                Video v2 = (Video)v1.DeepClone1();
                Console.WriteLine($"v2: Id={v2.Id},Content={v2.Content},ageList[0]={ v1.ageList[0]},Url={v2.vDetails.videoUrl},videoPic={v2.vDetails.videoPic}");
                //修改v1的值,v2不变
                v1.Content = "水浒传";
                v1.vDetails.videoUrl = @"H:\XXXXXXX";
                v1.vDetails.videoPic = 22222;
                Console.WriteLine($"v2: Id={v2.Id},Content={v2.Content},ageList[0]={ v2.ageList[0]},Url={v2.vDetails.videoUrl},videoPic={v2.vDetails.videoPic}");
            }
            #endregion

运行结果:

三. 原型模式详解

  1. 背景
     在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象,会比较复杂且耗时耗资源,用原型模式生成对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出很多孙悟空一样简单。

  2. 定义和特点
     定义如下:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。例如,Windows 操作系统的安装通常较耗时,如果复制就快了很多。

  3. 具体实现
    (1). 模式结构

 A. 抽象原型类:规定了具体原型对象必须实现的接口,eg:.Net 中的ICloneable。

 B. 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。

 C. 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

结构图如下:

(2). 使用场景

 有一块视频,我需要1个一模一样的,并且我还需要1个除了路径不同其它都相同的视频,这个时候可以使用原型模式哦。

(3). 代码实操

相关类:

/// <summary>
/// Video视频类
/// </summary>
[Serializable]
public class Video : ICloneable
{
    public string Id { set; get; } // 视频编号
    public string Content { set; get; } // 视频内容

    public List<int> ageList { set; get; }

    public VideoDetails vDetails { get; set; }

    /// <summary>
    /// 克隆方法(基于浅拷贝)
    /// 用于实现ICloneable里方法(当然你用来实现深拷贝也可以)
    /// </summary>
    /// <returns></returns>
    public object Clone()
    {
        return this.MemberwiseClone();
    }

    /// <summary>
    /// 克隆方法(基于深拷贝)
    /// 类前面必须加可序列化标志[Serializable]
    /// </summary>
    /// <returns></returns>
    public object DeepClone1()
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter bFormatter = new BinaryFormatter();
            bFormatter.Serialize(stream, this);
            stream.Seek(0, SeekOrigin.Begin);
            return bFormatter.Deserialize(stream);
        }
    }
}

/// <summary>
/// 视频详情类
/// </summary>
[Serializable]
public class VideoDetails
{
    public string videoUrl { get; set; }

    public int videoPic { get; set; }

    public VideoDetails(string myVideoUrl, int myVideoPic)
    {
        this.videoUrl = myVideoUrl;
        this.videoPic = myVideoPic;
    }
}

测试代码:

           #region 01-完全相同视频的拷贝
            {
                Console.WriteLine("------------------------------- 01-完全相同视频的拷贝------------------------------------");
                Video v1 = new Video()
                {
                    Id = "001",
                    Content = "西游记",
                    ageList = new List<int>() { 000 },
                    vDetails = new VideoDetails(@"H:\DesignMode", 1111)
                };
                Video v2 = (Video)v1.DeepClone1();  //深拷贝
                Console.WriteLine($"v1: Id={v1.Id},Content={v1.Content},ageList[0]={ v1.ageList[0]},Url={v1.vDetails.videoUrl},videoPic={v1.vDetails.videoPic}");

                Console.WriteLine($"v2: Id={v2.Id},Content={v2.Content},ageList[0]={ v2.ageList[0]},Url={v2.vDetails.videoUrl},videoPic={v2.vDetails.videoPic}");
            }
            #endregion


            #region 02-相似视频的拷贝
            {
                Console.WriteLine("-------------------------------02-相似视频的拷贝------------------------------------");
                Video v1 = new Video()
                {
                    Id = "001",
                    Content = "西游记",
                    ageList = new List<int>() { 000 },
                    vDetails = new VideoDetails(@"H:\DesignMode", 1111)
                };

                Video v2 = (Video)v1.DeepClone1();  //深拷贝
                Console.WriteLine($"v1: Id={v1.Id},Content={v1.Content},ageList[0]={ v1.ageList[0]},Url={v1.vDetails.videoUrl},videoPic={v1.vDetails.videoPic}");
                //相似视频的拷贝,只需要简单修改即可
                v2.vDetails.videoUrl = @"F:\newVideo";
                Console.WriteLine($"v2: Id={v2.Id},Content={v2.Content},ageList[0]={ v2.ageList[0]},Url={v2.vDetails.videoUrl},videoPic={v2.vDetails.videoPic}");
            }
            #endregion

运行结果:

  1. 使用场景
    (1). 对象之间相同或相似,即只是个别的几个属性不同的时候。

(2). 对象的创建过程比较麻烦,但复制比较简单的时候。

相关文章
|
1月前
|
设计模式 安全 Java
面向对象编程的精髓:Java设计模式 - 原型模式(Prototype)完全参考手册
【4月更文挑战第7天】原型模式是OOP中的创建型设计模式,用于通过复制现有实例创建新实例,尤其适用于创建成本高或依赖其他对象的情况。它包括Prototype接口、ConcretePrototype实现和Client客户端角色。优点是性能优化、避免子类化和动态增加产品族。实现包括定义原型接口、实现具体原型和客户端调用克隆方法。最佳实践涉及确保克隆正确性、选择深拷贝或浅拷贝及考虑线程安全。但需注意克隆方法管理、性能开销和循环引用等问题。在Java中,实现Cloneable接口和覆盖clone方法可实现原型模式。
|
1月前
|
设计模式 Java 关系型数据库
23种设计模式 —— 原型模式【克隆羊、浅拷贝、深拷贝】
23种设计模式 —— 原型模式【克隆羊、浅拷贝、深拷贝】
|
1月前
|
设计模式 安全 Java
【设计模式】原型模式
【设计模式】原型模式
|
1月前
|
设计模式 Java
Java设计模式【五】:原型模式
Java设计模式【五】:原型模式
21 0
|
1月前
|
设计模式 存储
二十三种设计模式全面解析-原型模式进阶之原型管理器:集中管理对象原型的设计模式之道
二十三种设计模式全面解析-原型模式进阶之原型管理器:集中管理对象原型的设计模式之道
|
3天前
|
设计模式 Java Spring
设计模式——原型模式
设计模式——原型模式
|
1月前
|
设计模式 测试技术 Go
[设计模式 Go实现] 创建型~ 原型模式
[设计模式 Go实现] 创建型~ 原型模式
|
1月前
|
设计模式 Java
【设计模式系列笔记】原型模式
原型模式(Prototype Pattern)是一种创建型设计模式,其主要目的是通过复制现有对象来创建新对象,而无需知道其具体类型。这种模式属于对象创建型模式,通过克隆来避免使用new关键字创建对象,提高性能和降低系统的耦合度。
41 6
|
1月前
|
设计模式 Java
23种设计模式,原型模式的概念优缺点以及JAVA代码举例
【4月更文挑战第10天】原型模式是一种创建型设计模式,它允许通过复制现有对象来创建新的对象,而无需知道如何创建的细节。这种模式的核心思想是基于一个原型实例,通过复制这个原型来创建新的对象
29 7
|
1月前
|
设计模式 Java Go
[设计模式Java实现附plantuml源码~创建型] 对象的克隆~原型模式
[设计模式Java实现附plantuml源码~创建型] 对象的克隆~原型模式