C#基础教程:第七讲:域与属性
第七讲 域与属性
域
域(Field)又称成员变量(Member Variable),它表示存储位置,是C#中类不可缺少的一部分。域的类型可以是C#中任何数据类型。但对于除去string类型的其他引用类型由于在初始化时涉及到一些类的构造器的操作,我们这里将不提及,我们把这一部分内容作为“类的嵌套”放在“接口 继承与多态”一讲内来阐述。
域分为实例域和静态域。实例域属于具体的对象,为特定的对象所专有。静态域属于类,为所有对象所共用。C#严格规定实例域只能通过对象来获取,静态域只能通过类来获取。例如我们有一个类型为MyClass的对象MyObject,MyClass内的实例域instanceField(存取限制为public)只能这样获取:MyObject. instanceField。而MyClass的静态域staticField(存取限制为public)只能这样获取:MyClass.staticField。注意静态域不能像传统C++那样通过对象获取,也就是说MyObject.staticField的用法是错误的,不能通过编译器编译。
域的存取限制集中体现了面向对象编程的封装原则。如前所述,C#中的存取限制修饰符有5种,这5种对域都适用。C#只是用internal扩展了C++原来的friend修饰符。在有必要使两个类的某些域互相可见时,我们将这些类的域声明为internal,然后将它们放在一个组合体内编译即可。如果需要对它们的继承子类也可见的话,声明为protected internal即可。实际上这也是组合体的本来意思--将逻辑相关的类组合封装在一起。
C#引入了readonly修饰符来表示只读域,const来表示不变常量。顾名思义对只读域不能进行写操作,不变常量不能被修改,这两者到底有什么区别呢?只读域只能在初始化--声明初始化或构造器初始化--的过程中赋值,其他地方不能进行对只读域的赋值操作,否则编译器会报错。只读域可以是实例域也可以是静态域。只读域的类型可以是C#语言的任何类型。但const修饰的常量必须在声明的同时赋值,而且要求编译器能够在编译时期计算出这个确定的值。const修饰的常量为静态变量,不能够为对象所获取。const修饰的值的类型也有限制,它只能为下列类型之一(或能够转换为下列类型的):sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum类型, 或引用类型。值得注意的是这里的引用类型,由于除去string类型外,所有的类型出去null值以外在编译时期都不能由编译器计算出他们的确切的值,所以我们能够声明为const的引用类型只能为string或值为null的其他引用类型。显然当我们声明一个null的常量时,我们已经失去了声明的意义--这也可以说是C#设计的尴尬之处!
这就是说,当我们需要一个const的常量时,但它的类型又限制了它不能在编译时期被计算出确定的值来,我们可采取将之声明为static readonly来解决。但两者之间还是有一点细微的差别的。看下面的两个不同的文件:
//file1.cs
|
我们的两个类分属于两个文件file1.cs 和file2.cs,并分开编译。在文件file1.cs内的域myField声明为static readonly时,如果我们由于某种需要改变了myField的值为20,我们只需重新编译文件file1.cs为file1.dll,在执行file2.exe时我们会得到20。但如果我们将static readonly改变为const后,再改变myField的初始化值时,我们必须重新编译所有引用到file1.dll的文件,否则我们引用的MyNamespace1.MyClass1.myField将不会如我们所愿而改变。这在大的系统开发过程中尤其需要注意。实际上,如果我们能够理解const修饰的常量是在编译时便被计算出确定的值,并代换到引用该常量的每一个地方,而readonly时在运行时才确定的量--只是在初始化后我们不希望它的值再改变,我们便能理解C#设计者们的良苦用心,我们才能彻底把握const和readonly的行为!
域的初始化是面向对象编程中一个需要特别注意的问题。C#编译器缺省将每一个域初始化为它的默认值。简单的说,数值类型(枚举类型)的默认值为0或0.0。字符类型的默认值为'/x0000'。布尔类型的默认值为false。引用类型的默认值为null。结构类型的默认值为其内的所有类型都取其相应的默认值。虽然C#编译器为每个类型都设置了默认类型,但作为面向对象的设计原则,我们还是需要对变量进行正确的初始化。实际上这也是C#推荐的做法,没有对域进行初始化会导致编译器发出警告信息。C#中对域进行初始化有两个地方--声明的同时进行初始化和在构造器内进行初始化。如前所述,域的声明初始化实际上被编译器作为赋值语句放在了构造器的内部的最开始处执行。实例变量初始化会被放在实例构造器内,静态变量初始化会被放在静态构造器内。如果我们声明了一个静态的变量并同时对之进行了初始化,那么编译器将为我们构造出一个静态构造器来把这个初始化语句变成赋值语句放在里面。而作为const修饰的常量域,从严格意义上讲不能算作初始化语句,我们可以将它看作类似于C++中的宏代换。
属性
属性可以说是C#语言的一个创新。当然你也可以说不是。不是的原因是它背后的实现实际上还是两个函数--一个赋值函数(get),一个取值函数(set),这从它生成的中间语言代码可以清晰地看到。是的原因是它的的确确在语言层面实现了面向对象编程一直以来对“属性”这一OO风格的类的特殊接口的诉求。理解属性的设计初衷是我们用好属性这一工具的根本。C#不提倡将域的保护级别设为public而使用户在类外任意操作--那样太不OO,或者具体点说太不安全!对所有有必要在类外可见的域,C#推荐采用属性来表达。属性不表示存储位置,这是属性和域的根本性的区别。下面是一个典型的属性设计:
using System; class Test |
一如我们期待的那样,程序输出0 1。我们可以看到属性通过对方法的包装向程序员提供了一个友好的域成员的存取界面。这里的value是C#的关键字,是我们进行属性操作时的set的隐含参数,也就是我们在执行属性写操作时的右值。
属性提供了只读(get),只写(set),读写(get和 set)三种接口操作。对域的这三种操作,我们必须在同一个属性名下声明,而不可以将它们分离,看下面的实现:
class MyClass public string Name |
上面这种分离Name属性实现的方法是错误的!我们应该像前面的例子一样将他们放在一起。值得注意的是三种属性(只读,只写,读写)被C#认为是同一个属性名,看下面的例子:
class MyClass class Test |
我们可以看到MyClassDerived中的属性Num-get{}屏蔽了MyClass中属性Num-set{}的定义。
当然属性远远不止仅仅限于域的接口操作,属性的本质还是方法,我们可以根据程序逻辑在属性的提取或赋值时进行某些检查,警告等额外操作,看下面的例子:
class MyClass |
由于属性的方法的本质,属性当然也有方法的种种修饰。属性也有5种存取修饰符,但属性的存取修饰往往为public,否则我们也就失去了属性作为类的公共接口的意义。除了方法的多参数带来的方法重载等特性属性不具备外, virtual, sealed, override, abstract等修饰符对属性与方法同样的行为,但由于属性在本质上被实现为两个方法,它的某些行为需要我们注意。看下面的例子:
abstract class A |
这个例子集中地展示了属性在继承上下文中的某些典型行为。这里,类A由于抽象属性Z的存在而必须声明为abstract。子类B中通过base关键字来引用父类A的属性。类B中可以只通过Y-set便覆盖了类A中的虚属性。
静态属性和静态方法一样只能存取类的静态域变量。我们也可以像做外部方法那样,声明外部属性。
C#基础教程:第八讲:索引器与操作符重载
索引器
索引器(Indexer)是C#引入的一个新型的类成员,它使得对象可以像数组那样被方便,直观的引用。索引器非常类似于我们前面讲到的属性,但索引器可以有参数列表,且只能作用在实例对象上,而不能在类上直接作用。下面是典型的索引器的设计,我们在这里忽略了具体的实现。
class MyClass |
索引器没有像属性和方法那样的名字,关键字this清楚地表达了索引器引用对象的特征。和属性一样,value关键字在set后的语句块里有参数传递意义。实际上从编译后的IL中间语言代码来看,上面这个索引器被实现为:
class MyClass |
由于我们的索引器在背后被编译成get_Item(int index)和set_Item(int index, object value)两个方法,我们甚至不能再在声明实现索引器的类里面声明实现这两个方法,编译器会对这样的行为报错。这样隐含实现的方法同样可以被我们进行调用,继承等操作,和我们自己实现的方法别无二致。通晓C#语言底层的编译实现为我们下面理解C#索引器的行为提供了一个很好的基础。
和方法一样,索引器有5种存取保护级别,和4种继承行为修饰,以及外部索引器。这些行为同方法没有任何差别,这里不再赘述。唯一不同的是索引器不能为静态(static),这在对象引用的语义下很容易理解。值得注意的是在覆盖(override)实现索引器时,应该用base[E]来存取父类的索引器。
和属性的实现一样,索引器的数据类型同时为get语句块的返回类型和set语句块中value关键字的类型。
索引器的参数列表也是值得注意的地方。“索引”的特征使得索引器必须具备至少一个参数,该参数位于this关键字之后的中括号内。索引器的参数也只能是传值类型,不可以有ref(引用)和out(输出)修饰。参数的数据类型可以是C#中的任何数据类型。C#根据不同的参数签名来进行索引器的多态辨析。中括号内的所有参数在get和set下都可以引用,而value关键字只能在set下作为传递参数。
下面是一个索引器的具体的应用例子,它对我们理解索引器的设计和应用很有帮助。
using System; |
编译并运行程序可以得到下面的输出:
True False True False True False True False True False
上面的程序通过索引器的使用为用户提供了一个界面友好的bool数组,同时又大大降低了程序的存储空间代价。索引器通常用于对象容器中为其内的对象提供友好的存取界面--这也是为什么C#将方法包装成索引器的原因所在。实际上,我们可以看到索引器在.NET Framework类库中有大量的应用。
操作符重载
操作符是C#中用于定义类的实例对象间表达式操作的一种成员。和索引器类似,操作符仍然是对方法实现的一种逻辑界面抽象,也就是说在编译成的IL中间语言代码中,操作符仍然是以方法的形式调用的。在类内定义操作符成员又叫操作符重载。C#中的重载操作符共有三种:一元操作符,二元操作符和转换操作符。并不是所有的操作符都可以重载,三种操作符都有相应的可重载操作符集,列于下表:
一元操作符 + - ! ~ ++ -- true false
二元操作符 + - * / % & | ^ << >> == != > < >= <=
转换操作符 隐式转换()和显式转换()
重载操作符必须是public和static 修饰的,否则会引起编译错误,这在操作符的逻辑语义下是不言而喻的。父类的重载操作符会被子类继承,但这种继承没有覆盖,隐藏,抽象等行为,不能对重载操作符进行virtual sealed override abstract修饰。操作符的参数必须为传值参数。我们下面来看一个具体的例子:
using System; |
编译程序并运行可得到下面的输出:
-3 + -4i 8 + 10i 3 + 4i 4 + 5i 3 + 4i 5 + 6i 5 + 6i
我们这里实现了一个“+”号二元操作符,一个“-”号一元操作符(取负值),和一个“++”一元操作符。注意这里,我们都没有对传进来的参数作任何改变--这在参数是引用类型的变量是尤其重要,虽然重载操作符的参数只能是传值方式。而我们在返回值时,往往需要“new”一个新的变量--除了true和false操作符。这在重载“++”和“--” 操作符时尤其显得重要。也就是说我们做在a++时,我们将丢弃原来的a值,而取代的是新的new出来的值给a! 值得注意的是e=a++或f=++a中e的值或f的值根本与我们重载的操作符返回值没有一点联系!它们的值仅仅是在前置和后置的情况下获得a的旧值或新值而已!前置和后置的行为不难理解。
操作符重载对返回值和参数类型有着相当严格的要求。一元操作符中只有一个参数。操作符“++”和“--”返回值类型和参数类型必须和声明该操作符的类型一样。操作符“+ - ! ~”的参数类型必须和声明该操作符的类型一样,返回值类型可以任意。true和false操作符的参数类型必须和声明该操作符的类型一样,而返回值类型必须为bool,而且必须配对出现--也就是说只声明其中一个是不对的,会引起编译错误。参数类型的不同会导致同名的操作符的重载--实际上这是方法重载的表现。
二元操作符参数必须为两个,而且两个必须至少有一个的参数类型为声明该操作符的类型。返回值类型可以任意。有三对操作符也需要必须配对声明出现,它们是“==”和“!=”,“>”和“<”,“>=”和“<=”。需要注意的是两个参数的类型不同,虽然类型相同但顺序不同都会导致同名的操作符的重载。
转换操作符为不同类型之间提供隐式转换和显式转换,主要用于方法调用,转型表达和赋值操作。转换操作符对其参数类型(被转换类型)和返回值类型(转换类型)也有严格的要求。参数类型和返回值类型不能相同,且两者之间必须至少有一个和定义操作符的类型相同。转换操作符必须定义在被转换类型或转换类型任何其中一个里面。不能对系统定义过的转换操作进行重新定义。两个类型也都不能是object或接口类型,两者之间不能有直接或间接的继承关系--这三种情况系统已经默认转换。我们来看一个例子:
using System; |
上面的例子提供了Digit类型和byte类型之间的隐式转换和显式转换。从Digit到byte的转换为隐式转换,转换过程不会因为丢失任何信息而抛出异常。从byte到Digit的转换为显式转换,转换过程有可能因丢失信息而抛出异常。实际上这也为我们揭示了什么时候声明隐式转换,什么时候声明显示转换的设计原则。不能对同一参数类型同时声明隐式转换和显式转换。隐式转换和显式转换无需配对使用--虽然C#推荐这样做。
实际上可以看到,对于属性,索引器和操作符这些C#提供给我们的界面操作,都是方法的某种形式的逻辑抽象包装,它旨在为我们定义的类型的用户提供一个友好易用的界面--我们完全可以通过方法来实现它们实现的功能。理解了这样的设计初衷,我们才会恰当,正确地用好这些操作,而不致导致滥用和错用。
C#基础教程:第九讲:处理鼠标和键盘事件
在程序运行中,产生事件的主体有很多,其中尤其以键盘和鼠标为最多。本文就来探讨一下在C#中和这二个主体相关的事件的处理过程。
一.本文介绍的程序设计和运行的软件环境:
(1).微软公司视窗2000服务器版
(2)..Net FrameWork SDK Beta 2
二.C#中处理鼠标相关的事件:
鼠标相关的事件大致有六种,分别是 :
"MouseHover"、"MouseLeave"、"MouseEnter"、"MouseMove"、"MouseDown"和"MouseUp"。
(1).如何在C#程序中定义这些事件:
在C#中是通过不同的Delegate来描述上述事件,其中描述"MouseHover"、"MouseLeave"、"MouseEnter"事件的Delegate是"EventHandler",而描述后面的三个事件的Delegate是"MouseEventHandler"来描述。这二个Delegate分别被封装在不同的命名空间,其中"EventHandler"被封装在"System"命名空间;"MouseEventHandler"被封装在"Syetem.Windows.Froms"命名空间中的。在为"MouseHover"、"MouseLeave"、"MouseEnter"事件通过数据的类是"EventArgs",他也被封装在"System"命名空间中;而为后面的三个事件提供数据的类是"MouseEventArgs",他却被封装在"Syetem.Windows.Froms"命名空间。以上这些就决定了在C#中定义这些事件和响应这些事件有着不同的处理办法。下面就来介绍这些不同点。
对于上述的前三个事件,是用以下语法来定义的:
"组件名称"."事件名称"+= new System.EventHandler("事件名称");
下面是程序中具体实现代码:
button1.MouseLeave += new Syetem.EvenHandler(button1_MLeave);
在完成了事件的定义以后,就要在程序中加入响应此事件的代码,否则程序编译的时候会报错。下面是响应上面事件的基本结构。
private void button1_MLeave ( object sender , System.EventArgs e ) |
定义"MouseMove"、"MouseDown"和"MouseUp"事件的语法和前面介绍的三个事件大致相同,具体如下:
"组件名称"."事件名称"+= new System.Windows.Forms. MouseEventHandler("事件名称");
下面是程序中具体实现代码:
button1.MouseMove += new System.Windows.Forms.MouseEventHandler(button1_MMove); |
下面是响应上面事件的基本结构:
private void button1_MMove ( object sender , System.Windows.Forms. MouseEventArgs e ) |
注释:在上述程序中的"button1"是定义的一个按钮组件。
(2).鼠标相关事件中的典型问题处理办法:
在掌握了C#中定义和鼠标相关的事件,我们就来探讨一下和鼠标相关事件的典型问题。其一是读取鼠标的当前位置;其二是判定到底是那个鼠标按键按动。
判定鼠标的位置可以通过事件"MouseMove"来处理,在"MouseEventArgs"类中提供了二个属性"X"和"Y",来判定当前鼠标纵坐标和横坐标。而判定鼠标按键的按动情况,可以通过事件"MouseDown"来处理,并且在"MouseEventArgs"类中也提供了一个属性"Button"来判定鼠标按键情况。根据这些知识,可以得到用C#编写的读取鼠标当前位置和判定鼠标按键情况的程序代码。下面就是此代码(mouse.cs)和此代码编译后运行界面:
|
mouse.cs的源程序代码如下:
using System ; public Form1 ( ) } private void Form1_MouseDown ( object sender , MouseEventArgs e ) |
三.C#中处理和键盘相关的事件:
在C#中和键盘相关的事件相对比较少,大致就三种:"KeyDown"、"KeyUp"和"KeyPress"。
(1).如何在C#程序中定义这些事件:
C#中描述"KeyDown"、"KeyUp"的事件的Delegate是"KeyEventHandler"。而描述"KeyPress"所用的Delegate是"KeyPressEventHandler"。这二个Delegate都被封装在命名空间"Syetem.Windows.Froms"中。为"KeyDown"、"KeyUp"的事件提供数据的类是"KeyEventArgs"。而为"KeyPress"事件提供数据的类是"KeyPressEventArgs"。同样这二者也被封装在命名空间"Syetem.Windows.Froms"中。
在C#程序定义"KeyDown"、"KeyUp"事件的语法如下:
"组件名称"."事件名称"+= new Syetem.Windows.Froms. KeyEventHandler("事件名称");
下面是程序中具体实现代码:
button1. KeyUp += new Syetem.Windows.Froms. KeyEventHandler(button1_KUp); |
下面是响应上面事件的基本结构。
private void button1_KUp ( object sender , Syetem.Windows.Froms. KeyEventArgs e ) |
在C#程序定义"KeyPress"事件的语法如下:
"组件名称"."事件名称"+= new Syetem.Windows.Froms. KeyPressEventHandler("事件名称");
下面是程序中具体实现代码:
button1. KeyPress += new Syetem.Windows.Froms. KeyPressEventArgs(button1_KPress); |
在完成了事件的定义以后,就要在程序中加入响应此事件的代码,否则程序编译的时候会报错。下面是响应上面事件的基本结构。
private void button1_KPress ( object sender , Syetem.Windows.Froms. KeyPressEventArgs e ) |
注释:程序中出现的"button1"是定义的一个按钮组件。
(2).和键盘相关事件中的典型问题处理办法:
和键盘相关的典型问题无非就是判定到底是哪个按键被按动。通过上面的三个事件都可以完成。并且在"KeyEventArgs"类中通过了一个属性"KeyCode",可以用他来读取当前按键。所以就在"KeyUp"或者"KeyDown"事件中处理这个问题。根据上面这些知识,可以得到用C#编写读取读取按键的程序代码,下面就是此代码(key.cs)和此代码运行后的界面:
|
key.cs的代码如下:
using System ; public Form1 ( ) } } |
四.总结:
本文介绍了在C#中如何定义和鼠标和键盘相关的事件和在这些事件中一些典型问题的处理办法。虽然这些知识最为基本,但也最为重要,因为在程序设计中,这些问题和我们打交道的机会最多。当然和鼠标和键盘相关的事件和问题还有许多,可以参照根据上面的解决办法加以解决。
C#基础教程:第十讲:API调用
介绍
API(Application Programming Interface),我想大家不会陌生,它是我们Windows编程的常客,虽然基于.Net平台的C#有了强大的类库,但是,我们还是不能否认API在Windows编程中的重要性。大多数的编程语言都支持API编程,而.Net平台中的MFC(Microsoft Foundation Class Library)构架本身就封装了大部分的API。
做为程序员,我们需要了解API从字面上了解便是编程接口,因此,做为开发者,需要了解的只是API的使用方法。
API根据操作系统、处理器及功能性的不同而拥有很多不同的类型。 操作系统特用的API:
每种操作系统都有许多通用的API以及一些特用的API,这些特用的API只能在当前操作系统中执行。
例如:
Windows NT 支持 MS-DOS, Win16, Win32, POSIX (Portable Operating System Interface), OS/2 console API; 而 Windows 95 支持 MS-DOS, Win16 以及 Win32 APIs.
Win16 & Win32 API:
Win16是为十六位处理器开发的,早期的操作系统均支持。
Win32则是为32位处理器开发。它可移植性强,被大部分的处理器所支持。
Win32 API在库名后有一个”
32”
后缀。比如KERNEL32,USER32等。
所有API在下面3个库中得以运行:
Kernel |
1. KERNEL
他的库名为 KERNEL32.DLL, 他主要用于产生与操作系统之间的关联:
程序加载
上下文选择.
文件输入输出.
内存管理.
例如: GlobalMemoryStatus 函数就包括当前物理内存及虚拟内存的使用信息。
2. USER
这个类库在Win32中名叫 USER32.DLL。
它允许管理全部的用户接口,比如:
窗口
菜单
对话框
图标等.,
例如: DrawIcon 函数将在指定的设备关联上“画”出图标或者鼠标。
3. GDI (Graphical Device Interface)
它在Win32中的库名为:GDI32.dll,它是图形输出库。使用GDI Windows“画”出窗口、菜单以及对话框等:
它能创建图形输出.
它也能保存图形文件.
例如: CreateBitmap 函数就能通过指定的长、宽、颜色创建一个位图。
C# 中操作API:
作为初学者来说,在C#中使用API确是一件令人头疼的问题。在使用API之间你必须知道如何在C#中使用结构、类型转换、安全/不安全代码,可控/不可控代码等许多知识。
一切从简单开始,复杂的大家一时不能接受。我们就从实现一个简单的MessageBox开始。首先打开VS.Net ,创建一个新的C#工程,并添加一个Button按钮。当这个按钮被点击,则显示一个MessageBox对话框。
即然我们需要引用外来库,所以必须导入一个Namespace:
using System.Runtime.InteropServices; |
接着添加下面的代码来声明一个API:
[DllImport("User32.dll")] |
此处DllImport属性被用来从不可控代码中调用一方法。”User32.dll”则设定了类库名。DllImport属性指定dll的位置,这个dll中包括调用的外部方法。Static修饰符则声明一个静态元素,而这个元素属于类型本身而不是上面指定的对象。extern则表示这个方法将在工程外部执行,使用DllImport导入的方法必须使用extern修饰符。
MessageBox 则是函数名,拥有4个参数,其返回值为数字。
大多数的API都能传递并返回值。
添中Click点击事件代码:
protected void button1_Click(object sender, System.EventArgs e) |
编译并运行这个程序,当你点击按钮后,你将会看到对话框,这便是你使用的API函数。
使用结构体
操作带有结构体的API比使用简单的API要复杂的多。但是一旦你掌握了API的过程,那个整个API世界将在你的掌握之中。
下面的例子中我们将使用GetSystemInfo API 来获取整个系统的信息。
第一步还是打开C#建立一个Form工程,同样的添中一个Button按钮,在代码窗中输入下面的代码,导入Namespace:
using System.Runtime.InteropServices; |
声明一个结构体,它将做为GetSystemInfo的一个参数:
|
声明API函数:
|
添加下面的代码至按钮的点击事件处理中:
首先创建一个SYSTEM_INFO结构体,并将其传递给GetSystemInfo函数。
|
一旦你接收到返回的结构体,那么就可以以返回的参数来执行操作了。
|
调用API全部代码
//Created By Ajit Mungale |
C#基础教程:第十一讲: 异常处理
|