《C#与.NET3.5高级程序设计(第4版)》笔记4

时间:2022-07-13 09:04:55

 第四章 C#核心编程结构2

 

本章对前一章进行补充,介绍了构造C#方法的细节,探讨了方法的各种关键字和方法重载的主题,之后介绍数组类型,也介绍了枚举类型、结构类型,然后详细介绍了值类型和引用类型之间区别,最后探讨了可空数据类型以及?和??运算符。

4.1方法重载和参数修饰符

C#中有四种参数修饰符,分别为:

  • (无),此时为值传递。数据的副本就会被传入函数,至于到底复制什么,取决于参数是值类型还是引用类型,前者复制值类型的本身,后者复制的是引用(类似于c++指针)。
  • out,输出参数,若参数用out声明,则调用时也必须加上out。使用前不用赋值,函数退出时,要给参数赋值,否则编译错误。
  • ref,引用参数,和out类似,区别主要有:

输出参数(out)无需(不是必须,赋的值也会在后面重新赋值时覆盖)传递前初始化,退出时必须给他赋值;

引用参数(ref)必须在传递前初始化,退出时可以(不是必须)改变他的值。

可以看出,以上两种方式的优点就是,只使用一次方法就可以获得多个返回值!而常规方式,只能用return返回一个值!

注意,即使将一个值类型的参数用上述两个修饰符进行声明,也会改变参数的值!

  • params,参数数组。可以把可变数量的参数(相同类型)作为单个逻辑参数传给方法。需要强调的是,这种方式声明的参数数组,在调用时,可以有两种方式,一种是传入强类型的数组(和用常规数组作为参数时一致),另一种是以逗号分割的项列表(这才是特有的方式),例如,下面的方法:
static double calculateaverage(int i,parmas double[] values) 

{ //something} 

//则调用时,可以用如下两种中任意一种哦: 

double[] b={1,2,3}; 

calculateaverage(100,b);//第一种方式 

calculateaverage(100,1,2,3);//第二种方式

第二种方式时,编译器会自动将后三个参数打包成数组,作为double[]参数传入。若不用params声明,第二种方式将出错!

成员重载:

是指一组名称相同,参数数量(或类型)不同的成员。

关键要确保方法的每一个版本都有不同的参数组(只是返回类型不同是不够唯一的),能够保证唯一的条件是:

  • 参数个数不同
  • 参数修饰符不同(out,ref,“无”等)
  • 参数类型不同(int,folat.......)

这种情况需要注意的是,虽然类型不同可以通过编译,但是若仅仅是参数类型不同(参数个数和修饰符等都无法区别的情况下),如果重载方法的参数类型之间存在着隐式转换关系(值类型和引用类型都存在此问题),那么有可能出现调用时候的一些问题:看如下情形:

         static void Main(string[] args)
        {
            TestArgsType(10);
            Console.Read();
        }
        static void TestArgsType(int args)
        {
            Console.WriteLine("int");
        }
        static void TestArgsType(uint args)
        {
            Console.WriteLine("uint");
        }         

则TestArgsType(10)执行的肯定是第一重载方法(int版的)的,这也许不是你想调用的方法,但是CLR就是这么处理了,因为这两个类型(uint和int)之间出现隐式转换关系,或者说是包含关系,这就有可能出现意外的调用结果。另外,如下面的代码,又将执行哪个方法呢(显然是long版的)

static void Main(string[] args)        
 {
     int i = 1;
     TestArgsType(i);
     Console.Read();
 }
static void TestArgsType(long args)
{
     Console.WriteLine("long");
}
static void TestArgsType(ulong args)
{
     Console.WriteLine("ulong");
} 

这里要引伸出来关于隐式转换的问题了,总结了一下,对于上面出现的这种问题,CLR在编译和运行时(若编译通过)按如下规则进行处理(值类型和引用类型均适用):

(1)对于引用类型,作为实参时,肯定你事先都指定为某种类型了,比如:

car mycar=new car();

testargstype(mycar);//testargstype为某个重载方法   

对于这种引用类型,可以继续看步骤(2)。

对于数值类型,有可能人为地指定了实参是某种类型,和上面的类似:

int c=10;

testargstype(c);

这种情况也可以继续看步骤(2)了。

还存在一种情况,就是没有为实参显式的指定为某种类型,如:

testargstype(10);

这种情况,就存在将这个给定的数值依照某种转换规则进行转换的过程,规则如下:

如果整数没有后缀,则其类型为以下类型中可表示其值的第一个类型:int、uint、longulong 。还可根据以下规则使用后缀指定文字类型:

  • 如果使用 L 或 l,那么根据整数的大小,可以判断出其类型为 long 还是 ulong 。(

    注意也可用小写字母“l”作后缀。但是,因为字母“l”容易与数字“1”混淆,会生成编译器警告。为清楚起见,请使用“L”。)

  • 如果使用U或u,那么根据整数的大小,可以判断出其类型为 uint 还是 ulong

  • 如果使用 UL、ul、Ul、uL、LU、lu、Lu 或 lU,则整数的类型为 ulong

无论如何,最终实参也会被转换为某种类型,那么也可以进入步骤(2)了。

(2) 上一步必然确定了实参的类型,那么这一步就是要去找是否有相应的重载方法,有三种情况:

  • 一种情况就是恰恰有和重载方法形参类型一致(比如实参确定为int型,而形参也是int型),那么显然是可以通过编译了,而且运行时调用的也是这个方法;
  • 如果没有找到形参类型完全一致的那个重载方法,如果存在兼容类型的形参参数也可以啊(也就是上面确定的实参类型可以隐式转换为重载方法的形参类型,例如int可以隐式转换为long,也或者实参为子类,形参为父类),这时也可以通过编译,并在运行时调用这个兼容重载方法;当然还有个问题需要解决的,那就是存在多个重载方法的形参都满足这种可兼容模式(比如实参int,而重载方法存在long和double两种兼容类型),那么到底调用哪个方法呢?基本规则就是在可隐式转换的那些重载函数中,根据形参按照由整型到浮点数,由取值范围小到取值范围大。给出一个助记方法:

这是一个由整型到浮点,由取值范围小到大的顺序构成的值类型链:

byte-sbyte-short-ushort-int-uint-long-ulong-float-double

对于任意一个实参,首先在链中找到自己的位置, 比如对于实参是int型,先会找到int的位置,然后会按照形参是否为uint-long-ulong-float-double的顺序查找,找到的第一个就是要调用的方法,再比如对于uint型实参,会按照long-ulong-float-double顺序查找(uint是无法隐式转换为int的哦)。

  • 若也不存在兼容类型的,那么编译将无法通过哦。

可以看出,若调用重载方法时不显式的指定参数类型,且重载方法参数存在隐式转换或继承关系,那么有可能导致意外的调用结果,因此,在调用时事先给出实参类型,设计重载方法时尽量不设计有可能导致上述问题的重载方法,应该是一个好习惯。

4.2数组

数组当然是一组相同类型的数据点。下界从0开始。

new(声明数组)后,若不显式填充,则每项都给予默认值。

总结一下数组的初始化方法:

(1)逐个填充

int[] a=new int[3];//使用此方法时,必须指定数组大小;这样才能默认填充这么多个值,此时,所有数组中值均为0,下面可以对不想是默认的项逐个赋值。

a[0]=1;

a[1]=2;

(2)花括号初始化

以下三种方式均可:

  • int[] a=new int[]{1,2,3};
  • int[] a=new int[3]{1,2,3};
  • int[] a={1,2,3};

也就是说,不需要指定数组大小,因为可以通过花括号的项来推断;new关键字是可选的。

若数组大小指定了,却和花括号中项数不一致,反而出错,因此,推荐第三种,简单!

在c#数组中,可以定义引用类型的数组,如datatable类型,string类型,甚至是object的数组,这时候,不仅需要对整个数组进行实例化,还需要对其中每一个项进行实例化,记住,每一项还需要进行实例化哦。

如:

object[] myobject=new object[2];

myobject[0]=10; //此项为值类型,直接赋值

myobject[1]=new datatime(1988,1,3);//为引用类型,需要实例化

多维数组:

包括了矩形数组和交错数组。

矩形数组:每一行的长度都相同的多维数组。

int[,] a =new int[1,2];

交错数组:

包含一些内部数组,每一个都有各自的上界。

int[][] a=new int [5][3];

这些数组的声明方法和初始化,有兴趣的可以查看相关资料,这里不再赘述。

另外,数组可以作为参数和返回值。

4.3枚举

枚举用于创建一组符号名和已知数字值的类型。

定义方法:

enum emptype 

{ 

manager, 

grnut=10, 

contractor, 

} 

注意,最后一个逗号可以有也可以没有!默认情况下,第一个元素若没有赋值,则被默认是0,若其中任意一个没有赋值,则默认是上一个成员的数值加1,则可以推断,contractor为11。这些值不一定连续,也不一定唯一,没有限制。

默认时,枚举值的存储类型为int类型,若想更改,则可以使用冒号来设置,C#支持byte,sbyte,short,ushort,int,uint,long,ulong。

使用方法:

enum emptype:byte 

{ 

manager, 

grnut=10, 

contractor, 

}; //可以有结尾分号 

枚举成员的访问权限隐含为public 。

对于enum的使用,一般就是直接用点运算符来访问里面的符号名,也可以根据符号名,获取对应的整数值(需要强制转换),例如:

string thisstr=emptype.manager.Tostring();

byte thisbyte=(byte)emptype.manager;

对于enum,我不经常使用,对它的用法,觉得也比较别扭。

例如可以用new实例化这个枚举,但似乎也没什么用途。

emptype a =  new emptype();//这句没错

再比如下个用法:

emptype a=emptype.manager;

若要获取枚举中的符号名名称,用a.tostring()即可,若要获取a对应的数值,必须根据底层存数类型,进行强制转换

(byte)a,这是由于,虽然emptype中的各个符号对应的是数值,但是并非真的是数值类型,因此必须转换成为数值。

另外,下面两个t1和t2竟然是一个类型~,确实是用的不大习惯。     

emptype t1 = emptype .test1;
emptype t2 = new emptype ();

  if (t1 == t2)
  {
  //do something
  }

看到这里,如果对于面向对象有一定基础的话,也许会问这个枚举和静态类的静态变量和常量有什么区别吗?

public static class Day
{
   public  const int Sun = 1;//常量哦
   public static int Mon = 2;//静态变量 

}

从不变性来看,虽然const数据成员的确是存在的,但其含义却不是我们所期望的(静态变量也是如此)。const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const数据成员的值可以不同(const数据成员的初始化能在类构造函数的初始化中进行)。怎样才能建立在整个类中都恒定的常量呢?别指望const数据成员了,应该用类中的枚举常量来实现。枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如PI=3.14159)。

从直观性来看,C# 枚举和常量应用区别之枚举的定义中加入描述(Description)。

从性能上看,在C#中,枚举的真正强大之处是它们在后台会实例化为派生于基类System.Enum的结构。这表示可以对它们调用方法,执行有用的任务。注意因为.NET Framework的执行方式,在语法上把枚举当做结构是不会有性能损失的。实际上,一旦代码编译好,枚举就成为基本类型,与int和float类似。

详细请看:《C# 枚举和常量应用区别浅析》http://developer.51cto.com/art/200908/144474.htm

实际上,枚举的应用也是比较广泛的,虽然其他的类型也能实现他的功能,但是在性能等方面,枚举具有不可替代的作用,有时间也应该深入研究一下。

4.4结构

结构是可以包含许多数据字段和操作这些字段的成员的用户自定义类型。结构有许多类似于类的特性,可以看成是轻量级的类类型。结构可以定义构造函数,实现接口,还包含许多属性、方法、事件以及重载运算符。但是结构无法继承。

初始化一个结构体,有两种方式,一种是创建变量后,为每一个公共字段数据赋值。

例如定义一个结构体:

struct point
        {
            public int a;
            public int b;
            public void display()
            {
                Console.WriteLine("a:{0},b:{1}",a, b);
            }
        } 

则使用时:

point p;

p.a=1;

p.b=2;

p.display();

另外一种方式是使用new关键字来创建变量,他会调用结构的默认构造函数(因此,结构中不允许再定义没有参数的构造函数,也就是这个系统提供的默认构造函数无法覆盖),并自动为公共字段赋予默认值。如:

point p=new point();

p.display();

还一种方式,就是利用自定义的构造函数,这需要在定义结构时显式进行定义才可使用。

结构体的应用也是十分广泛的,由于在语法和操作上与类类型有相似之处,这里在说明两者区别后,不再详述结构体,在后面介绍类类型时,这里应该有更好的理解了。

4.5值类型与引用类型

值类型都隐式派生自system.valuetype,其唯一目的是重写了由system.object定义的虚方法来使用基于值而不是基于引用的语法,重写会改变定义在基类中的虚(也可能是抽象的)方法的实现,事实上,valuetype定义的实例方法和由object定义的完全相同。由于值类型使用基于值的语法、结构,生命期可以预测。

当用赋值运算符(=)将一种类型赋值给另外一个时,有两种情况:

将一个值类型赋值给另外一个时,对字段成员逐一进行复制(对于int这样简单类型,唯一需要复制的成员就是数值,对于结构,如上point,则a,b的值都会被复制到新的结构变量中。)栈上会有这个类型的两个副本,每个都被独立操作,一个的变化,不影响另外一个。

将一个引用类型赋值给另外一个时,在内存中重定向引用变量的指向。两个引用指向托管堆中的同一个对象,因此,其中一个的改变,同时会影响另外一个。

包含引用类型的值类型:

若在一个值类型中包含引用类型,比如结构中有一个引用类型,则将这个结构赋值给另外一个结构时,将会把结构中值类型进行赋值,而将结构中的引用进行复制,因此,更改结构中的值类型不影响另外一个结构中对应的值类型,而两个结构中的实例由于保存的是都是这个对象的引用,改变其中一个实例的内容,必然影响另外一个结构中实例的内容。 (非常感谢redjackwong 更正了叙述问题)

这就是所谓的“浅复制”,还有一种称为“深复制”,即将内部引用的状态完全复制到一个新的对象中去,需要实现ICLoneable结构,之后的篇章中会讲到。

按值传递和按引用传递引用类型:

本章一开始就说了参数有四种类型,其中就有按值传递(无修饰符)和按引用传递(ref和out(这两个的区别前面以后说了,以下不再分开说)),

对于一个值类型,按值传递实参时,方法中对于形参的操作,不会影响实参值,若用引用传递,则形参的改变,也会导致实参的变化:

	static void Main(string[] args)
        {
            int a = 1;
            testint(a);
            Console.WriteLine(a);//a还是1
            testint(ref a);
            Console.WriteLine(a);//a是2了
        }
        static void testint( int a)
        {
            a = 2;
        }
        static void testint(ref int a)
        {
            a = 2;
        }

引用类型,采用值传递和引用传递,效果就又是一个样子,他们都可以改变实参的内容,唯一不同的就是是否能改变实参的指向(因为实参就是一个引用)

//按值传递一个引用类型(如person)时,如:
static void send(person p)
{
p.age=99;// 这句是有效的
p=new person("lee",99);// 这句是无效的,不会影响到实参的指向 

}
//按引用传递一个引用类型(如person)时,如:
static void send(ref person p)
{
p.age=99;// 这句是有效的
p=new person("lee",99);// 这句是有效的,实参的指向改变了 

}

前者类似于p是一个常量指针,可以访问和修改它指向的对象内容,但是不能更改它的指向(new)。

后者类似于可变的指针,可以访问和修改它指向的对象,也可以更改它的指向,使之重新指向另外一个对象。
这个区别是在很多书籍中都没有介绍过的!

再简单总结一下一些遗漏的值类型与引用类型的区别:
前者不能被继承,后者可以;前者默认构造函数是系统提供的,用户不能自定义一个没有参数的构造函数,后者当然可以。
4.6可空类型

.NET2.0发布后,支持一种可空类型。它就是可以表示所有基础类型的值加上null,为了定义一个可空变量类型,应在底层数据类型中添加问号(?)作为后缀进行声明。
注意,这种类型只对值类型合法,定义一个可空引用类型是不合法的(引用类型本身就可以为null!)
定义方法:

int? a=10;
int?[] b=new int?[5];
Nullable<int> c=10;
public int? getsomething()
{
}

注:?后缀记法是创建一个泛型system.nullable<T>的缩写。
可以通过hasvalue或!=运算符判断是否为空。
对于这个可空类型,可以通过value属性或直接的获取这个值(可以是空)。
最后,可以使用??运算符,在当获取的值是null时,将一个值赋值给这个类型,
这相当于一个判定语句,当为空时赋一个值,不为空时,不理睬。如:
int? a=dr.getsomtthing()??100
如果dr.getsomtthing()返回一个空值null,则将100赋值给a,否则将dr.getsomtthing()的返回值赋值给a。

类似如下语句:

int? a;
if (dr.getsomtthing() is Nullable)//之前少了个括号(,再次多谢 redjackwong 了!
  {
    a=dr.getsomtthing();
  }
else
  {
    a=100;
  }