C# 常用的对象操作
1.对象的类型判断
任何一个对象它都具有双重类型的身份,即声明类型和实际类型,正因为对象有了双重的类型身份,因此也出现了类型兼容的概念。同时也因为对象的引用性质,也使它在判断相等的概念上具有多重意义(即存在引用相等和值相等)。
首先我们来看类型兼容,看一段代码:
using System; class Fruit { } class Apple:Fruit { } class Test { public static void Main() { Fruit f1= new Fruit(); Fruit f2= new Apple(); //类型通过虚函数表中GetType()函数获得 Type t1=f1.GetType(); //Fruit Type t2=f2.GetType(); //Apple Console.WriteLine(t1); Console.WriteLine(t2); Console.WriteLine(t1==t2); if (f1 is Apple) { Apple a1=(Apple)f1; } } } |
在这里我们定义了两个类型Fruit和Apple,我们让Apple继承自Fruit。在客户端我们实例化了两个对象f1,f2,他们的声明类型相同,但是实际类型是否也相同了,这里我们不能只凭表面上的判断就说它们不相等,我们的做法是,先获得f1和f2的实际类型,在比较它们的类型是否相等。在这里GetType()方法就是用来获取一个对象的实际类型,这里额外说一下,对象的GetType()方法是一个从Object继承下来的虚方法,存储于对象虚函数指针所指向的虚函数表中,通过这个方法获取类型信息以后,就可以判断它们是否相等了。注意,在这里我们判断相等都是判断对象的实际类型。
下面我们看一下is和as操作符,它们都是判断类型是否兼容的。再看上面的代码,如果我们不写if 语句,直接写进行转型操作,那么C#编译器会抛出类型安全的异常,也就是说在进行转型之前,C#编译器会自动进行类型安全的检查,判断对象是否能够被转型,即判断被转对象的类型与目标类型是否兼容,那么在这里C#编译器自动帮我们进行了类型的兼容性检查,这是在C#中一个独有的特性,以往的C++不会进行类型兼容性的检查,默认的就可以进行转型。那么为了提高系统的性能,C#给我们提供了两种进行类型兼容性判断的操作符is和as,我们看上面if语句中的代码,它是什么意思了,如果f1的类型是f1的实际类型或者是该实际类型的父类,那么返回true,否则返回false,即兼容是一种向上的兼容。只有满足了兼容性,才能进行安全的类型转换。那么我们上面的代码就进行了两次兼容性的判断。
as操作符的与is操作符稍有不同,不同之处在于判断兼容性以后的返回值不同,如果兼容,则返回转型后的对象,如果不兼容,就返回null值。下面看一段代码:
Apple a2=f1 as Apple;
if (a2!=null)
{
Console.WriteLine(a2);
}
在这里我们只进行了一次兼容性的判断,因为在判断兼容性的同时就可以返回转型后的对象,而使用is操作符的时候一次手动的兼容性的判断,还有一次类型转换时默认的类型兼容判断。那么as操作符了is操作符分别用于什么场合了,如果只需要进行类型兼容的检查,那么就用is操作符,如果在兼容型检查的时候同时需要进行类型转换,那么就使用as操作符。在实际编程的过程中,这些做法都是不被鼓励的,一旦做了类型判断,就是在用一种分解的思维来解决问题,那么就抛弃了多态的思维。
2.对象的相等判断
判断对象的相等有三种方式,操作符:==,!=,虚方法(重写后值相等):Object.Equals,静态方法:Object.Equals、Object.referenceEquals
1.操作符==,!=,预定义引用相等,重载后值相等。
2.Object.Equals虚方法,重写后值相等
3.Object.Equals静态方法,属于Object类,当虚方法Object.Equals被重写以后,可以判断值相等。
4.Object.referenceEquals静态方法,属于Object类,判断引用相等。
重写顺序:先重写虚方法objA.Equals(ObjB)(缺点:无法保证objA不为null),但此时静态方法Object.Equals(ObjA,ObjB)却能够判断值相等且可以保证objA不为null(根据微软对这个方法的定义),因此我们可以利用静态方法Object.Equals(ObjA,ObjB)继续进行操作符的重载。
这些方法都是用于进行引用相等的判断的,如果要进行值相等的判断,必须进行重写。下面我们看一段代码:
using System; class Point { public int x; public int y; public Point( int x, int y) { this .x=x; this .y=y; } } class Test { public static void Main() { Point p1= new Point(100,200); Point p2= new Point(100,200); Point p3= new Point(200,400); Point p4=p1; //针对引用类型判断的是地址相等 Console.WriteLine(p1==p2); //false Console.WriteLine(p1==p3); //false Console.WriteLine(p1==p4); //true } } |
因为==操作符默认为判断引用相等,因此结果为false,false,true.如果我们需要判断值相等了,就需要重写上面的一些虚方法。
using System; class Point { public int x; public int y; public Point( int x, int y) { this .x=x; this .y=y; } public override bool Equals(Object obj) //重写虚函数,提供值相等的比较 { if (obj== null ) { return false ; } if (obj== this ) //判断是否为同一对象 { return true ; } if ( this .GetType()!=obj.GetType()) //判断类型是否相同 { return false ; } Point other=(Point)obj; if ( this .x==other.x && this .y==other.y) { return true } else { return false ; } } } class Test { public static void Main() { Point p1= new Point(100,200); Point p2= new Point(100,200); Point p3= new Point(200,400); Point p4=p1; Console.WriteLine(p1.Equals(p2)); //true Console.WriteLine(p1.Equals(p3)); //false Console.WriteLine(p1.Equals(p4)); //true } } |
因为不同类型的内部实现不一样,其对象在判断相等时依据也不一样,因此值相等的判断方法需要根据具体的类而重写。因此C#语言并未提供值相等的比较方法。那么我们在这里为了提供值相等的比较方法,我们重写了虚函数Equals,这个虚函数的参数固定为Object型,对于这个虚函数,如果不进行重写,它进行的是引用相等。关于这个方法的重写,这里我们一点一点来分析,首先,我们判断了比较的对象是否为空,如果为空,则直接返回false。如果没有这一步且obj为空时,则程序会抛出异常。其次我们判断了相比较的两个对象是否为同一对象,如果是,就直接返回true,不需要再进行内部细节的比较,这一做法完全是为了提高比较效率,可以省略。接下来我们判断了两个对象的类型是否相等(不是兼容),因为类型相同是对象进行值相等比较的前提条件,如果类型不相同,就没有必要在进行值相等的比较了。
判断到这里,我们开始进行内部细节的比较,因为已经判断了类型的相等,因此不必要再用as操作符来进行类型兼容检查的类型转换,直接进行转换即可,转换以后就可以进行内部细节的比较了。
讲到这里,我们可能会注意到前面的一句话,Object.Equals静态方法,我们一定想知道为什么当虚方Object.Equals被重写以后它就可以进行值相等的判断了,下面我先给出这个方法的实现代码:
public static bool Equals(Object obj1,Object obj2) //静态方法
{
if (obj1==obj2)
{
return true;
}
if (obj1==null||obj2==null)
{
return false;
}
return obj1.Equals(obj2); //调用虚方法
}
这个静态方法在Object类中,是微软已经写好了的,在这个方法里面我们最后调用了虚方法Object.Equals,如果该方法没有被重写,那么它判断的是引用相等,导致该静态方法也是进行引用相等的判断,如果被重写了,那么虚方法和静态方法都是判断值相等。可能有人要问,为什么有了一个虚方法,还需要一个静态的方法了,这个方法解决了调用该方法的对象为空的问题,在下面操作符的重载中我们会遇到这个问题。
操作符重载:所有的重载操作符都是静态的,为了能让==号也能进行值相等的判断,下面我们对==操作符进行重载(重载而不是重写),首先我们看一段操作符重载代码:
public static bool operator==(Point p1,Point p2) //操作符重载
{
return p1.Equals(p2);
}
因为我们的虚方法Equals已经进行了重写,可以用于进行值相等的判断了,而且在这里可以保证p2不为空,但是并不能保证p1也不为空,因此这种做法是不可取的,在这里我们就需要使用静态方法Object.Equals,它可以保证p1和p2均不为空。
public static bool operator==(Point p1,Point p2) //操作符重载
{
return Object.Equals(p1,p2);
}
重载了==操作符,同时也要重载!=号操作符,这是一个规则。
public static bool operator!=(Point p1,Point p2) //!=操作符重载
{
return !Object.Equals(p1,p2);
}
下面我贴出完整的代码:
using System;
class Point
{
public int x;
public int y;
public Point(int x,int y)
{
this.x=x;
this.y=y;
}
public override bool Equals(Object obj) //重写虚函数,提供值相等的比较
{
if (obj==null)
{
return false;
}
if (obj==this) //判断是否为同一对象
{
return true;
}
if (this.GetType()!=obj.GetType()) //判断类型是否相同
{
return false;
}
Point other=(Point)obj;
if (this.x==other.x && this.y==other.y)
{
return true
}
else
{
return false;
}
}
//由于虚方法Equals已经进行了重写,因此该静态方法可以用于值相等的判断。
public static bool operator==(Point p1,Point p2) //==操作符重载
{
return Object.Equals(p1,p2);
}
public static bool operator!=(Point p1,Point p2) //!=操作符重载
{
return !Object.Equals(p1,p2);
}
}
class Test
{
public static void Main()
{
Point p1=new Point(100,200);
Point p2=new Point(100,200);
Point p3=new Point(200,400);
Point p4=p1;
Console.WriteLine(p1.Equals(p2)); //true
Console.WriteLine(p1.Equals(p3)); //false
Console.WriteLine(p1.Equals(p4)); //true
}
}
/* 微软已经写好的两个静态方法
public static bool Equals(Object obj1,Object obj2) //静态方法
{
if (obj1==obj2)
{
return true;
}
if (obj1==null||obj2==null)
{
return false;
}
return obj1.Equals(obj2); //调用虚方法
}
public static bool referenceEquals(Object obj1,Object obj2)
{
retrun obj1==obj2;
}
*/
如果我们要进行引用相等的判断,现在只能用静态方法referenceEquals,它永远是进行引用相等的判断。对于referenceEquals静态方法的实现,我们可能会感到疑惑,如果==号操作符被重载了,那么是不是就进行的值相等的判断了?其实不然,这里仍然进行的是引用相等的判断,原因是:==重载符是基于编译时绑定的(JIT编译),他需要根据==两边的声明类型来判断是进行值相等的判断还是引用相等的判断,在上面的代码中,我们是基于Point类型来进行==操作符的重载,因此当编译时编译器就会对referenceEquals方法传入参数的声明类型进行判断,如果为Point就进行值相等的判断,否则就进行引用相等的判断。
关于referenceEquals方法对值类型变引用判断,下面我们看一段代码:
class Test
{
public static void Main()
{
int data=100;
Console.WriteLine(Object.referenceEquals(data,data));
}
}
很多人可能会觉得它的结果应该是true,因为它们是同一对象,其实不然,因为referenceEquals方法所接受的两个参数都是Object类型,那么在将两个int型参数传给该方法的时候,会进行两次装箱操作,这样会在托管堆上生成两个相同的对象,因此这里判断为false.
3.对象的克隆(Clone)
深克隆:对象克隆之后完全相等,无共享成分,即栈上和堆上的内存均进行克隆。
浅克隆:对象克隆之后完全相等,有共享成分,即只复制栈上的内存。
我们先看一段代码:
using System;
class point
{
private int x;
private int y;
public Point(int x,int y)
{
this.x=x;
this.y=y;
}
}
class Test
{
public static void Main()
{
Point p1=new Point(10,20);
Point p2=p1;
}
}
这段代码中,p1和p2指向了堆上相同的对象,那么如果我们想让p1和p2分别指向两个相同的对象,我们应该如何做了,这里就需要用到克隆。首先我们需要被克隆的对象的类实现了克隆接口,提供给外部克隆的方法,该方法要求返回值必须是Object.下面我们给出代码:
using System;
class point : ICloneable
{
private int x;
private int y;
public Point(int x,int y)
{
this.x=x;
this.y=y;
}
public object Clone()
{
Point p=new Point(this.x,this.y)
return p;
}
}
class Test
{
public static void Main()
{
Point p1=new Point(10,20);
Point p2=(Point)p1.Clone()
}
}
说到这里我们需要介绍一个方法MemberwiseClone(),这个方法会进行按成员拷贝,即把对象的第一层内存完全拷贝,包括指针,因此按成员拷贝值适用于对象成员只有值类型的情况(如果有引用类型,则只会拷贝指针,这样会出现共享,不安全。),而上面的Point类正好符合要求,因此上面的代码中的拷贝行为我们可以这样实现:
using System;
class point : ICloneable
{
private int x;
private int y;
public Point(int x,int y)
{
this.x=x;
this.y=y;
}
public object Clone()
{
return this.MemberwiseClone();
}
}
class Test
{
public static void Main()
{
Point p1=new Point(10,20);
Point p2=(Point)p1.Clone()
}
}
下面我们来看一个稍微复杂一点的。
using System;
class point : ICloneable //Point类
{
private int x;
private int y;
public Point(int x,int y)
{
this.x=x;
this.y=y;
}
public object Clone() //实现ICloneable接口
{
Point p=new Point(this.x,this.y)
return p;
}
}
class Rectangle:ICloneable //Rectangle类
{
private int width;
private int height;
Point p;
public Rectangle(int width,int height,int x,int y)
{
this.width=width;
this.height=height;
this.p=new Point(x,y);
}
public object Clone() //实现ICloneable接口
{
Rectangle r=new Rectangle();
r.width=this.width;
r.height=this.height;
r.p=this.p;
return r;
}
}
class Test
{
public static void Main()
{
Rectangle r1=new Rectangle(10,20,30,40);
Rectangle r2=(Rectangle)r1.Clone();
}
}
此时,r1克隆以后得到r2,注意,这里是浅克隆,r1和r2有共享的部分,浅克隆在很多时候是不正确的。如果r1被修改,那么r2与r1共享的部分也会被修改(string除外)。那么我们应该如何实现深克隆了,在这里我们修改上面的代码:
using System;
class point : ICloneable //Point类
{
private int x;
private int y;
public Point(int x,int y)
{
this.x=x;
this.y=y;
}
public object Clone() //实现ICloneable接口
{
Point p=new Point(this.x,this.y)
return p;
}
}
class Rectangle:ICloneable //Rectangle类
{
private int width;
private int height;
Point p;
public Rectangle(int width,int height,int x,int y)
{
this.width=width;
this.height=height;
this.p=new Point(x,y);
}
public object Clone() //实现ICloneable接口
{
Rectangle r=new Rectangle();
r.width=this.width;
r.height=this.height;
if (this.p!=null)
{
r.p=(Point)this.p.Clone();
}
return r;
}
}
class Test
{
public static void Main()
{
Rectangle r1=new Rectangle(10,20,30,40);
Rectangle r2=(Rectangle)r1.Clone();
}
}
上面的代码中我们修改了对p对象的拷贝方法,实现了深克隆,但前提是Point类也必须实现了ICloneable接口,且Point类实现的是深克隆。在这里由于Point类只包含x和y两个字段,均为值类型,因此拷贝以后无共享成分。
4.数组与集合对象克隆
这种克隆仅克隆集合中的元素,不克隆元素所引用的字段,即是一种浅克隆,数组默认有一个克隆函数,不需要实现接口。下面我们看一段代码:
using System;
class Point
{
private int x;
private int y;
public Point(int x,int y)
{
this.x=x;
this.y=y;
}
}
class Test
{
public static void Main()
{
Point[] points=new Point[10];
for(int i=0;i<points.Length;i++)
{
Point[i]=new Point(i*10,i*20);
}
Point[] points2=(Point[])points.Clone();
}
}
posted on 2011-11-04 22:37 guowenhui 阅读(1984) 评论(1) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架