(二)类、对象、方法和作用域_第1页
(二)类、对象、方法和作用域_第2页
(二)类、对象、方法和作用域_第3页
(二)类、对象、方法和作用域_第4页
(二)类、对象、方法和作用域_第5页
已阅读5页,还剩70页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、类、对象、方法和作用域一、什么是类和对象 二、构造与析构 三、什么是方法 四、声明方法 五、调用方法 六、编写方法 七、运用作用域 八、多态与new关键字 九、常用函数和方法 十、对象封装和类 十一、集合对象 十二、类的私有成员 十三、装箱与拆箱 十四、C#类的继承机制一、什么是类和对象 组件编程不是对传统面向对象的抛弃,相反组件编程正是面向对象编程的深化和发展。类作为面向对象的灵魂在C#语言里有着相当广泛深入的应用,很多非常“Sharp”的组件特性甚至都是直接由类包装而成。对类的深度掌握自然是我们“Sharp XP”重要的一环。 类 C#的类是一种对包括数据成员,函数成员和嵌套类型进行封装的

2、数据结构。其中数据成员可以是常量,域。函数成员可以是方法,属性,索引器,事件,操作符,实例构建器,静态构建器,析构器。我们将在“第五讲 构造器与析构器”和“第六讲 域 方法 属性与索引器”对这些成员及其特性作详细的剖析。除了某些导入的外部方法,类及其成员在C#中的声明和实现通常要放在一起。 C#用多种修饰符来表达类的不同性质。根据其保护级C#的类有五种不同的限制修饰符: public可以被任意存取; protected只可以被本类和其继承子类存取; internal只可以被本组合体(Assembly)内所有的类存取,组合体是C#语言中类被组合后的逻辑单位和物理单位,其编译后的文件扩展名往往是“

3、.DLL”或“.EXE”。 protected internal唯一的一种组合限制修饰符,它只可以被本组合体内所有的类和这些类的继承子类所存取。 private只可以被本类所存取。 如果不是嵌套的类,命名空间或编译单元内的类只有public和internal两种修饰。 new修饰符只能用于嵌套的类,表示对继承父类同名类型的隐藏。 abstract用来修饰抽象类,表示该类只能作为父类被用于继承,而不能进行对象实例化。抽象类可以包含抽象的成员,但这并非必须。abstract不能和new同时用。下面是抽象类用法的伪码: abstract class Apublic abstract void F()

4、;abstract class B: Apublic void G() class C: Bpublic override void F() /方法F的实现抽象类A内含一个抽象方法F(),它不能被实例化。类B继承自类A,其内包含了一个实例方法G(),但并没有实现抽象方法F(),所以仍然必须声明为抽象类。类C继承自类B,实现类抽象方法F(),于是可以进行对象实例化。 sealed用来修饰类为密封类,阻止该类被继承。同时对一个类作abstract和sealed的修饰是没有意义的,也是被禁止的。 对象与this关键字 类与对象的区分对我们把握OO编程至关重要。我们说类是对其成员的一种封装,但类的封装

5、设计仅仅是我们编程的第一步,对类进行对象实例化,并在其数据成员上实施操作才是我们完成现实任务的根本。实例化对象采用MyClass myObject=new MyClass()语法,这里的new语义将调用相应的构建器。C#所有的对象都将创建在托管堆上。实例化后的类型我们称之为对象,其核心特征便是拥有了一份自己特有的数据成员拷贝。这些为特有的对象所持有的数据成员我们称之为实例成员。相反那些不为特有的对象所持有的数据成员我们称之为静态成员,在类中用static修饰符声明。仅对静态数据成员实施操作的称为静态函数成员。C#中静态数据成员和函数成员只能通过类名引用获取,看下面的代码: using Syst

6、em;class Apublic int count;public void F()Console.WriteLine(this.count);public static string name;public static void G()Console.WriteLine(name);class Testpublic static void Main()A a1=new A();A a2=new A();a1.F();a1.count=1;a2.F();a2.count=2;A.name=CCW;A.G();我们声明了两个A对象a1,a2。对于实例成员count和F(),我们只能通过a1,a

7、2引用。对于静态成员name和G()我们只能通过类型A来引用,而不可以这样,或a1.G()。 在上面的程序中,我们看到在实例方法F()中我们才用this来引用变量count。这里的this是什么意思呢?this 关键字引用当前对象实例的成员。在实例方法体内我们也可以省略this,直接引用count,实际上两者的语义相同。理所当然的,静态成员函数没有 this 指针。this 关键字一般用于从构造函数、实例方法和实例访问器中访问成员。 在构造函数中this用于限定被相同的名称隐藏的成员,例如: class Employeepublic Employee(string name, s

8、tring alias) = name;this.alias = alias;将对象作为参数传递到其他方法时也要用this表达,例如: CalcTax(this);声明索引器时this更是不可或缺,例如: public int this int paramgetreturn arrayparam;setarrayparam = value;System.Object类C#中所有的类都直接或间接继承自System.Object类,这使得C#中的类得以单根继承。如果我们没有明确指定继承类,编译器缺省认为该类继承自System.Object类。System.Object类也可用小

9、写的object关键字表示,两者完全等同。自然C#中所有的类都继承了System.Object类的公共接口,剖析它们对我们理解并掌握C#中类的行为非常重要。下面是仅用接口形式表示的System.Object类: namespace Systempublic class Objectpublic static bool Equals(object objA,object objB)public static bool ReferenceEquals(object objA,object objB)public Object()public virtual bool Equals(object o

10、bj)public virtual int GetHashCode()public Type GetType()public virtual string ToString()protected virtual void Finalize()protected object MemberwiseClone()我们先看object的两个静态方法Equals(object objA,object objB),ReferenceEquals(object objA,object objB)和一个实例方法Equals(object obj)。在我们阐述这两个方法之前我们首先要清楚面向对象编程两个重要的

11、相等概念:值相等和引用相等。值相等的意思是它们的数据成员按内存位分别相等。引用相等则是指它们指向同一个内存地址,或者说它们的对象句柄相等。引用相等必然推出值相等。对于值类型关系等号“= =”判断两者是否值相等(结构类型和枚举类型没有定义关系等号“= =”,我们必须自己定义)。对于引用类型关系等号“= =”判断两者是否引用相等。值类型在C#里通常没有引用相等的表示,只有在非托管编程中采用取地址符“&”来间接判断二者的地址是否相等。 静态方法Equals(object objA,object objB)首先检查两个对象objA和objB是否都为null,如果是则返回true,否则进行objA.Eq

12、uals(objB)调用并返回其值。问题归结到实例方法Equals(object obj)。该方法缺省的实现其实就是return this= =obj;也就是判断两个对象是否引用相等。但我们注意到该方法是一个虚方法,C#推荐我们重写此方法来判断两个对象是否值相等。实际上Microsoft.NET框架类库内提供的许多类型都重写了该方法,如:System.String(string),System.Int32(int)等,但也有些类型并没有重写该方法如:System.Array等,我们在使用时一定要注意。对于引用类型,如果没有重写实例方法Equals(object obj),我们对它的调用相当于t

13、his= =obj,即引用相等判断。所有的值类型(隐含继承自System.ValueType类)都重写了实例方法Equals(object obj)来判断是否值相等。 注意对于对象x,x.Equals(null)返回false,这里x显然不能为null(否则不能完成Equals()调用,系统抛出空引用错误)。从这里我们也可看出设计静态方法Equals(object objA,object objB)的原因了-如果两个对象objA和objB都可能为null,我们便只能用object. Equals(object objA,object objB)来判断它们是否值相等了-当然如果我们没有改写实例方

14、法Equals(object obj),我们得到的仍是引用相等的结果。我们可以实现接口IComparable(有关接口我们将在“第七讲 接口 继承与多态”里阐述)来强制改写实例方法Equals(object obj)。 对于值类型,实例方法Equals(object obj)应该和关系等号“= =”的返回值一致,也就是说如果我们重写了实例方法Equals(object obj),我们也应该重载或定义关系等号“= =”操作符,反之亦然。虽然值类型(继承自System.ValueType类)都重写了实例方法Equals(object obj),但C#推荐我们重写自己的值类型的实例方法Equals(

15、object obj),因为系统的System.ValueType类重写的很低效。对于引用类型我们应该重写实例方法Equals(object obj)来表达值相等,一般不应该重载关系等号“= =”操作符,因为它的缺省语义是判断引用相等。 静态方法ReferenceEquals(object objA,object objB)判断两个对象是否引用相等。如果两个对象为引用类型,那么它的语义和没有重载的关系等号“= =”操作符相同。如果两个对象为值类型,那么它的返回值一定是false。 实例方法GetHashCode()为相应的类型提供哈希(hash)码值,应用于哈希算法或哈希表中。需要注意的是如果

16、我们重写了某类型的实例方法Equals(object obj),我们也应该重写实例方法GetHashCode()-这理所应当,两个对象的值相等,它们的哈希码也应该相等。下面的代码是对前面几个方法的一个很好的示例: using System;struct Apublic int count;class Bpublic int number;class Cpublic int integer=0;public override bool Equals(object obj)C c=obj as C;if (c!=null)return eger=eger;elseretu

17、rn false;public override int GetHashCode()return 2integer;class Testpublic static void Main()A a1,a2;a1.count=10;a2=a1;/Console.Write(a1=a2);没有定义“= =”操作符Console.Write(a1.Equals(a2);/TrueConsole.WriteLine(object.ReferenceEquals(a1,a2);/FalseB b1=new B();B b2=new B();b1.number=10;b2.number=10;Console.

18、Write(b1=b2);/FalseConsole.Write(b1.Equals(b2);/FalseConsole.WriteLine(object.ReferenceEquals(b1,b2);/Falseb2=b1;Console.Write(b1=b2);/TrueConsole.Write(b1.Equals(b2);/TrueConsole.WriteLine(object.ReferenceEquals(b1,b2);/TrueC c1=new C();C c2=new C();eger=10;eger=10;Console.Write(c1=c2);

19、/FalseConsole.Write(c1.Equals(c2);/TrueConsole.WriteLine(object.ReferenceEquals(c1,c2);/Falsec2=c1;Console.Write(c1=c2);/TrueConsole.Write(c1.Equals(c2);/TrueConsole.WriteLine(object.ReferenceEquals(c1,c2);/True如我们所期望,编译程序并运行我们会得到以下输出: TrueFalse FalseFalseFalse TrueTrueTrue FalseTrueFalse TrueTrueTr

20、ue 实例方法GetType()与typeof的语义相同,它们都通过查询对象的元数据来确定对象的运行时类型,我们在“第十讲 特征与映射”对此作详细的阐述。 实例方法ToString()返回对象的字符串表达形式。如果我们没有重写该方法,系统一般将类型名作为字符串返回。 受保护的Finalize()方法在C#中有特殊的语义,我们将在“第五讲 构造器与析构器”里详细阐述。 受保护的MemberwiseClone()方法返回目前对象的一个“影子拷贝”,该方法不能被子类重写。“影子拷贝”仅仅是对象的一份按位拷贝,其含义是对对象内的值类型变量进行赋值拷贝,对其内的引用类型变量进行句柄拷贝,也就是拷贝后的引

21、用变量将持有对同一块内存的引用。相对于“影子拷贝”的是深度拷贝,它对引用类型的变量进行的是值复制,而非句柄复制。例如X是一个含有对象A,B引用的对象,而对象A又含有对象M的引用。Y是X的一个“影子拷贝”。那么Y将拥有同样的A,B的引用。但对于X的一个“深度拷贝”Z来说,它将拥有对象C和D的引用,以及一个间接的对象N的引用,其中C是A的一份拷贝,D是B的一份拷贝,N是M的一份拷贝。深度拷贝在C#里通过实现ICloneable接口(提供Clone()方法)来完成。 对对象和System.Object的把握为类的学习作了一个很好的铺垫,但这仅仅是我们锐利之行的一小步,关乎对象成员初始化,内存引用的释

22、放,继承与多态,异常处理等等诸多“Sharp”特技堪为浩瀚,让我们继续期待下面的专题! 二、构造与析构 构造器 构造器负责类中成员变量(域)的初始化。C#的类有两种构造器:实例构造器和静态构造器。实例构造器负责初始化类中的实例变量,它只有在用户用new关键字为对象分配内存时才被调用。而且作为引用类型的类,其实例化后的对象必然是分配在托管堆(Managed Heap)上。这里的托管的意思是指该内存受.NET的CLR运行时管理。和C+不同的是,C#中的对象不可以分配在栈中,用户只声明对象是不会产生构造器调用的。 实例构造器分为缺省构造器和非缺省构造器。缺省构造器是在一个类没有声明任何构造器的情况下

23、,编译器强制为该类添加的一个无参数的构造器,该构造器仅仅调用父类的无参数构造器。缺省构造器实际上是C#编译器为保证每一个类都有至少一个构造器而采取的附加规则。注意这里的三个要点: 子类没有声明任何构造器;编译器为子类加的缺省构造器一定为无参数的构造器; 父类一定要存在一个无参数的构造器。 看下面例子的输出: using System;public class MyClass1public MyClass1()Console.WriteLine(“MyClass1Parameterless Contructor!”);public MyClass1(string param1)Console.W

24、riteLine(“MyClass1 Constructor Parameters : ”+param1);public class MyClass2:MyClass1public class Testpublic static void Main()MyClass2 myobject1=new MyClass2();编译程序并运行可以得到下面的输出: MyClass1 Parameterless Contructor! 读者可以去掉MyClass1的无参构造器public MyClass1()看看编译结果。 构造器在继承时需要特别的注意,为了保证父类成员变量的正确初始化,子类的任何构造器默认

25、的都必须调用父类的某一构造器,具体调用哪个构造器要看构造器的初始化参数列表。如果没有初始化参数列表,那么子类的该构造器就调用父类的无参数构造器;如果有初始化参数列表,那么子类的该构造器就调用父类对应的参数构造器。看下面例子的输出: using System;public class MyClass1public MyClass1()Console.WriteLine(MyClass1 Parameterless Contructor!);public MyClass1(string param1)Console.WriteLine(MyClass1 Constructor Parameters

26、 : +param1);public class MyClass2:MyClass1public MyClass2(string param1):base(param1)Console.WriteLine(MyClass2 Constructor Parameters : +param1);public class Testpublic static void Main()MyClass2 myobject1=new MyClass2(Hello);编译程序并运行可以得到下面的输出: MyClass1 Constructor Parameters : Hello MyClass2 Constr

27、uctor Parameters : Hello C#支持变量的声明初始化。类内的成员变量声明初始化被编译器转换成赋值语句强加在类的每一个构造器的内部。那么初始化语句与调用父类构造器的语句的顺序是什么呢?看下面例子的输出: using System;public class MyClass1public MyClass1() Print();public virtual void Print() public class MyClass2: MyClass1int x = 1;int y;public MyClass2() y = -1;Print();public override void

28、 Print() Console.WriteLine(x = 0, y = 1, x, y);public class Teststatic void Main() MyClass2 MyObject1 = new MyClass2();编译程序并运行可以得到下面的输出: x = 1, y = 0 x = 1, y = -1 容易看到初始化语句在父类构造器调用之前,最后执行的才是本构造器内的语句。也就是说变量初始化的优先权是最高的。 我们看到类的构造器的声明中有public修饰符,那么当然也可以有protected/private/ internal修饰符。根据修饰符规则,我们如果将一个类的构

29、造器修饰为private,那么我们在继承该类的时候,我们将不能对这个private的构造器进行调用,我们是否就不能对它进行继承了吗?正是这样。实际上这样的类在我们的类内的成员变量都是静态(static)时,而又不想让类的用户对它进行实例化,这时必须屏蔽编译器为我们暗中添加的构造器(编译器添加的构造器都为public),就很有必要作一个private的实例构造器了。protected/internal也有类似的用法。 类的构造器没有返回值,这一点是不言自明的。 静态构造器初始化类中的静态变量。静态构造器不象实例构造器那样在继承中被隐含调用,也不可以被用户直接调用。掌握静态构造器的要点是掌握它的执

30、行时间。静态构造器的执行并不确定(编译器没有明确定义)。但有四个准则需要掌握: 在一个程序的执行过程中,静态构造器最多只执行一次。 静态构造器在类的静态成员初始化之后执行。或者讲编译器会将静态成员初始化语句转换成赋值语句放在静态构造器执行的最开始。 静态构造器在任何类的静态成员被引用之前执行。 静态构造器在任何类的实例变量被分配之前执行。 看下面例子的输出: using System;class MyClass1static MyClass1() Console.WriteLine(MyClass1 Static Contructor);public static void Method1()

31、 Console.WriteLine(MyClass1.Method1);class MyClass2static MyClass2() Console.WriteLine(MyClass2 Static Contructor);public static void Method1() Console.WriteLine(MyClass2.Method1);class Teststatic void Main() MyClass1.Method1();MyClass2.Method1();编译程序并运行可以得到下面的输出: MyClass1 Static Contructor MyClass1

32、.Method1 MyClass2 Static Contructor MyClass2.Method1 当然也可能输出: MyClass1 Static Contructor MyClass2 Static Contructor MyClass1.Method1 MyClass2.Method1 值得指出的是实例构造器内可以引用实例变量,也可引用静态变量。而静态构造器内能引用静态变量。这在类与对象的语义下是很容易理解的。 实际上如果我们能够深刻地把握类的构造器的唯一目的就是保证类内的成员变量能够得到正确的初始化,我们对各种C#中形形色色的构造器便有会心的理解-它没有理由不这样!构器 由于.N

33、ET平台的自动垃圾收集机制,C#语言中类的析构器不再如传统C+那么必要,析构器不再承担对象成员的内存释放-自动垃圾收集机制保证内存的回收。实际上C#中已根本没有delete操作!析构器只负责回收处理那些非系统的资源,比较典型的如:打开的文件,获取的窗口句柄,数据库连接,网络连接等等需要用户自己动手释放的非内存资源。我们看下面例子的输出: using System;class MyClass1MyClass1() Console.WriteLine(MyClass1s destructor);class MyClass2: MyClass1MyClass2() Console.WriteLine

34、(MyClass2s destructor);public class Testpublic static void Main() MyClass2 MyObject = new MyClass2();MyObject = null;GC.Collect();GC.WaitForPendingFinalizers();编译程序并运行可以得到下面的输出: MyClass2s destructor MyClass1s destructor 其中程序中最后两句是保证类的析构器得到调用。GC.Collect()是强迫通用语言运行时进行启动垃圾收集线程进行回收工作。而GC.WaitForPendingF

35、inalizers()是挂起目前的线程等待整个终止化(Finalizaion)操作的完成。终止化(Finalizaion)操作保证类的析构器被执行,这在下面会详细说明。 析构器不会被继承,也就是说类内必须明确的声明析构器,该类才存在析构器。用户实现析构器时,编译器自动添加调用父类的析构器,这在下面的Finalize方法中会详细说明。析构器由于垃圾收集机制会被在合适的的时候自动调用,用户不能自己调用析构器。只有实例析构器,而没有静态析构器。 那么析构器是怎么被自动调用的?这在 .Net垃圾回收机制由一种称作终止化(Finalizaion)的操作来支持。.Net系统缺省的终止化操作不做任何操作,如

36、果用户需要释放非受管资源,用户只要在析构器内实现这样的操作即可-这也是C#推荐的做法。我们看下面这段代码: using System;class MyClass1MyClass1()Console.WritleLine(MyClass1 Destructor);而实际上,从生成的中间代码来看我们可以发现,这些代码被转化成了下面的代码: using System;class MyClass1protected override void Finalize()tryConsole.WritleLine(My Class1 Destructor); finallybase.Finalize();实际

37、上C#编译器不允许用户自己重载或调用Finalize方法-编译器彻底屏蔽了父类的Finalize方法(由于C#的单根继承性质,System.Object类是所有类的祖先类,自然每个类都有Finalize方法),好像这样的方法根本不存在似的。我们看下面的代码实际上是错的: using System;class MyClassoverride protected void Finalize() / 错误public void MyMethod() this.Finalize();/ 错误但下面的代码却是正确的: using System;class MyClasspublic void Final

38、ize()Console.WriteLine(My Class Destructor);public class Testpublic static void Main() MyClass MyObject=new MyClass();MyObject.Finalize();实际上这里的Finalize方法已经彻底脱离了“终止化操作”的语义,而成为C#语言的一个一般方法了。值得注意的是这也屏蔽了父类System.Object的Finalize方法,所以要格外小心! 终止化操作在.Net运行时里有很多限制,往往不被推荐实现。当对一个对象实现了终止器(Finalizer)后,运行时便会将这个对象的

39、引用加入一个称作终止化对象引用集的队列,作为要求终止化的标志。当垃圾收集开始时,若一个对象不再被引用但它被加入了终止化对象引用集的队列,那么运行时并不立即对此对象进行垃圾收集工作,而是将此对象标志为要求终止化操作对象。待垃圾收集完成后,终止化线程便会被运行时唤醒执行终止化操作。显然这之后要从终止化对象引用集的链表中将之删去。而只有到下一次的垃圾收集时,这个对象才开始真正的垃圾收集,该对象的内存资源才被真正回收。容易看出来,终止化操作使垃圾收集进行了两次,这会给系统带来不小的额外开销。终止化是通过启用线程机制来实现的,这有一个线程安全的问题。.Net运行时不能保证终止化执行的顺序,也就是说如果对

40、象A有一个指向对象B的引用,两个对象都有终止化操作,但对象A在终止化操作时并不一定有有效的对象A引用。.Net运行时不允许用户在程序运行中直接调用Finalize()方法。如果用户迫切需要这样的操作,可以实现IDisposable接口来提供公共的Dispose()方法。需要说明的是提供了Dispose()方法后,依然需要提供Finalize方法的操作,即实现假托的析构函数。因为Dispose()方法并不能保证被调用。所以.Net运行时不推荐对对象进行终止化操作即提供析构函数,只是在有非受管资源如数据库的连接,文件的打开等需要严格释放时,才需要这样做。 大多数时候,垃圾收集应该交由.Net运行时

41、来控制,但有些时候,可能需要人为地控制一下垃圾回收操作。例如在操作了一次大规模的对象集合后,我们确信不再在这些对象上进行任何的操作了,那我们可以强制垃圾回收立即执行,这通过调用System.GC.Collect() 方法即可实现,但频繁的收集会显著地降低系统的性能。还有一种情况,已经将一个对象放到了终止化对象引用集的链上了,但如果我们在程序中某些地方已经做了终止化的操作,即明确调用了Dispose()方法,在那之后便可以通过调用System.GC.SupressFinalize()来将对象的引用从终止化对象引用集链上摘掉,以忽略终止化操作。终止化操作的系统负担是很重的。 在深入了解了.NET运

42、行时的自动垃圾收集功能后,我们便会领会C#中的析构器为什么绕了这么大的弯来实现我们的编程需求,才能把内存资源和非内存资源的回收做的游刃有余-这也正是析构的本原! 三、什么是方法方法又称成员函数(Member Function),集中体现了类或对象的行为。方法同样分为静态方法和实例方法。静态方法只可以操作静态域,而实例方法既可以操作实例域,也可以操作静态域-虽然这不被推荐,但在某些特殊的情况下会显得很有用。方法也有如域一样的5种存取修饰符-public,protected,internal,protected internal,private,它们的意义如前所述。 方法参数方法的参数是个值得特别

43、注意的地方。方法的参数传递有四种类型:传值(by value),传址(by reference),输出参数(by output),数组参数(by array)。传值参数无需额外的修饰符,传址参数需要修饰符ref,输出参数需要修饰符out,数组参数需要修饰符params。传值参数在方法调用过程中如果改变了参数的值,那么传入方法的参数在方法调用完成以后并不因此而改变,而是保留原来传入时的值。传址参数恰恰相反,如果方法调用过程改变了参数的值,那么传入方法的参数在调用完成以后也随之改变。实际上从名称上我们可以清楚地看出两者的含义-传值参数传递的是调用参数的一份拷贝,而传址参数传递的是调用参数的内存地址

44、,该参数在方法内外指向的是同一个存储位置。看下面的例子及其输出: using System;class Teststatic void Swap(ref int x, ref int y) int temp = x;x = y;y = temp;static void Swap(int x,int y) int temp = x;x = y;y = temp;static void Main() int i = 1, j = 2;Swap(ref i, ref j);Console.WriteLine(i = 0, j = 1, i, j);Swap(i,j);Console.WriteLin

45、e(i = 0, j = 1, i, j);程序经编译后执行输出: i = 2, j = 1 i = 2, j = 1 我们可以清楚地看到两个交换函数Swap()由于参数的差别-传值与传址,而得到不同的调用结果。注意传址参数的方法调用无论在声明时还是调用时都要加上ref修饰符。 笼统地说传值不会改变参数的值在有些情况下是错误的,我们看下面一个例子: using System;class Elementpublic int Number=10;class Teststatic void Change(Element s)s.Number=100;static void Main() Elemen

46、t e=new Element();Console.WriteLine(e.Number);Change(e); Console.WriteLine(e.Number);程序经编译后执行输出: 10 100 我们看到即使传值方式仍然改变了类型为Element类的对象t。但严格意义上讲,我们是改变了对象t的域,而非对象t本身。我们再看下面的例子: using System;class Elementpublic int Number=10;class Teststatic void Change(Element s)Element r=new Element();r.Number=100;s=r

47、;static void Main() Element e=new Element();Console.WriteLine(e.Number);Change(e); Console.WriteLine(e.Number);程序经编译后执行输出: 10 10传值方式根本没有改变类型为Element类的对象t!实际上,如果我们能够理解类这一C#中的引用类型(reference type)的特性,我们便能看出上面两个例子差别!在传值过程中,引用类型本身不会改变(t不会改变),但引用类型内含的域却会改变(t.Number改变了)!C#语言的引用类型有:object类型(包括系统内建的class类型和用

48、户自建的class类型-继承自object类型),string类型,interface类型,array类型,delegate类型。它们在传值调用中都有上面两个例子展示的特性。 在传值和传址情况下,C#强制要求参数在传入之前由用户明确初始化,否则编译器报错!但我们如果有一个并不依赖于参数初值的函数,我们只是需要函数返回时得到它的值是该怎么办呢?往往在我们的函数返回值不至一个时我们特别需要这种技巧。答案是用out修饰的输出参数。但需要记住输出参数与通常的函数返回值有一定的区别:函数返回值往往存在堆栈里,在返回时弹出;而输出参数需要用户预先制定存储位置,也就是用户需要提前声明变量-当然也可以初始化。

49、看下面的例子: using System;class Teststatic void ResoluteName(string fullname,out string firstname,out string lastname) string strArray=fullname.Split(new char );firstname=strArray0;lastname=strArray1;public static void Main() string MyName=Cornfield Lee;string MyFirstName,MyLastName;ResoluteName(MyName,out MyFirstName,out MyLastName);Console.WriteLine(My first name: 0, My last name: 1, MyFirstName, MyLastName);程序经编译后执行输出: My first name: Cornfield, My last name: Lee 在函数体内所有输出参数必须被赋值,否则编译器报错!out修饰符同样

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论