C# 常用的对象操作

来源:互联网 发布:java可变长参数 数组 编辑:程序博客网 时间:2024/06/06 18:20

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 (f1is 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();             }        }
复制代码
原创粉丝点击