第三章 C#语言基础
3.1、剖析一个简单的C#程序
1.简单示例
C#要求所有的程序逻辑都包含在一个类型定义中。
using System;
class HelloClass
{
public static int Main(string[] args)
{
Console.WriteLine("Hello World!");
Console.ReadLine();
return 0;
}
}
需要理解:公共成员能通过其他类型访问;静态成员的作用于在类级别上而非对象级别上,并且不需要事先创建一个新的类实例就能被调用。
C#区分大小写。C#关键字都是小写的,命名空间、类型和成员名称以一个大写字母开头,中间的单词是首字母大写。
2、Main方法的其他形式
可以带参数或不带,可以返回int或void。
Main()方法在vs2005里自动定义成private的,其他程序集不能直接调用该应用程序的入口。
如何构造Main()取决于:是否需要处理用户提供的命令行参数;完成时是否需要向系统返回一个值。
3、处理命令行参数
可以直接访问args,用for或foreach遍历。
也可以通过System.Enviroment类型的静态方法GetCommandLineArgs()来访问,返回的是字符串数组,第一个索引表示包含应用程序本身的当前目录,其余的元素包含单独的命令行参数。使用时,不用再将Main()定义成一个带字符串数组参数的方法。
使用VS2005指定命令行参数:Solution Explore里的Debug那里指定Command line arguments。
3.2、System.Enviroment类
包含很多静态函数可以获得操作系统的细节。例如:OsVersion, CurrentDirectory, GetLogicDrives, Version, MchineName, NewLine, ProcessorCount, SystemDirectory, UserName。
3.3、初步了解类及对象
与C++不同,C#不允许将类类型分配到栈上,因此必须使用new关键字来建立对象。
事实上,C#的对象变量是对内存中对象的引用。
1、有关构造函数
默认构造函数会为每一个成员数据设置一个合适的默认值。支持重载构造函数。
一旦为一个类型定义了自定义的构造函数,默认构造函数就被删除了。如果希望允许对象用户使用默认构造函数创建类型的实力,需要显示的重定义它。
2、C#没有析构函数
不用显式的销毁一个对象,垃圾回收机制会自动释放所分配的内存。
3、定义“应用程序对象”
有关“分工:(separation of concerns)。类应该只定义自己相关的功能,调用自己的最好放到另外一个类中。例如HelloClass类和HelloApp类。
也就是说,最好不要在main方法里创建定义自己的那个类型的实例。
3.4、System.Console类
该类封装了基本控制台应用程序的输入、输出和错误流操作。
表3.1:该类在.NET2.0中特有的成员
成员 |
作用 |
BackgroundCokor ForegroupColor |
设置当前输出的背景/前景色。可以被赋予ConsoleColor枚举的任何成员。 |
BufferHeight BufferWidth |
控制控制台缓冲区与的现实区域 |
Clear() |
清楚缓存和控制台的标题 |
Title |
设置当前控制台的标题 |
WindowHeight WindowWidth WindowTop WindowLeft |
控制与已建立的缓冲区相关的控制台大小 |
1、使用Console类进行输入和输出
该类的输入输出方法都被定义成静态的。
可以用大括号数字编码来制定可选占位符。比如下面几种方法:
Console.WriteLine("Int is: {0}/nDouble is: {1}/nBool is: {2}", theInt, theDouble, theBool);
object[] stuff = {"Hello", 20.9, 1, "There", "83", 99.99933};
Console.WriteLine("The Stuff is: {0}, {1}, {2}, {3}, {4}, {5}", stuff);
Console.WirteLine("{0}, Number {0}, Number {0}", 9);
2、.NET字符串格式化标志
表3.2 .NET字符串格式字符
字符串格式字符 |
作用 |
C或c |
用于格式化货币。默认情况下,这个表示会以当地的货币符号作为前缀。 |
D或d |
用于格式化十进制数。这个标记还用于指定填充值的最小个数。 |
E或e |
用于指定记数法。 |
F或f |
用于定点小数的格式化。 |
G或g |
代表general。这个字符用来格式化一个数为定点或指数格式。 |
N或n |
用于基本的数值格式化(带逗号)。 |
X或x |
用于十六进制格式化。如果使用大写的X,十六进制格式也会包含大写的字符。 |
例如下面程序:
Console.WriteLine("{0}, Number{0}, Number{0}", 9);
Console.WriteLine("C format: {0:C}", 99989.987);
Console.WriteLine("D9 format: {0:D9}", 99999);
Console.WriteLine("E format: {0:E}", 99999.76543);
Console.WriteLine("F3 format: {0:F3}", 99999.9999);
Console.WriteLine("N format: {0:N}", 99999);
Console.WriteLine("X format: {0:X}", 99999);
Console.WriteLine("x format: {0:x}", 99999);
应该意识到.NET格式化字符的使用不仅仅局限于控制台应用程序,同样能用在静态和String.Format()方法的上下文中。例如:
string formatStr;
formatStr = String.Format("Don't you wish you had {0:C} in your account?", 99989.987);
Console.WriteLine(formatStr);
3.5、设置成员可见性
对于给定类或结构的一个成员(方法、字段、构造函数等)必须指定它们的“可见性”级别。
如果为显示指定,则默认为private。
表3.3 C#的可访问性关键字
C#访问修饰符 |
作用 |
public |
成员既可以从一个对象变量访问,又可以从任何派生类访问。 |
private |
成员仅能被这个类的方法访问。所有的成员默认为private。 |
protected |
成员既可以在定义它的类中使用,又可以在任何派生类中使用。然而,它不能从对象变量访问。 |
internal |
成员可以被同一个程序集内的任何类型访问,但是不能被程序集外被任何类型访问。 |
protected internal |
成员的访问被限制在当前程序集,或者当前程序集中从定义它的类所派生的类型中。 |
可见以下示例:
class SomeClass
{
// Accessible anywhere.
public void PublicMethod() { }
// Accessible only from SomeClass types.
private void PrivateMethod() { }
// Accessible from SomeClass and any descendent.
protected void ProtectedMethod() { }
// Accessible from within the same assembly.
internal void InternalMethod() { }
// Assembly-protected access.
protected internal void ProtectedInternalMethod() { }
// Unmarked members are private by default in C#.
void SomeMethod() { }
}
1、有关类型可见性
类型(类、接口、结构、枚举和委托)也可以带访问修饰符,但是只能用public或internal。
类型默认为internal的。
3.6、类成员变量的默认值
1、类类型的成员变量
bool类型被设置成false。
数值类型被设置为0,如果是浮点数据类型的话为0.0。
string类型的被设置为null。
char类型的被设置为'/0'。
引用类型被设置成null。
2、局部变量
当定义局部变量的时候,它们不接受默认赋值,必须在使用它们之前赋一个初始值。
有一个例外,如果这个变量用作输出参数,则不需要被赋予初始值。
3.7、成员变量的初始化语法
类类型的成员可以在定义该成员时赋予初始值,但是要注意成员的复制发生在构造函数之前,因此如果在构造函数里又给一个字段赋值,那么实际上就取代了前面的成员赋值。
3.8、定义常量数据
使用const关键字,用来定义
不应该被再次赋值的数据。不同于C++,在C#中const关键字不能被用来限定参数或返回值,而被保留用来创建
局部或
实例一级的数据。
要赋给常量的值必须在编译期间就知道的,因此常量成员不能被赋给对象的引用。
必须在声明时直接赋值,而不能放到构造函数里。
如果用ildasm来查看这些常量,会发现这些值是直接硬编码到程序集中的。
常量字段隐含为静态的,因此如果引用外部类型定义的常量,需要以定义它的类型的名字开头。
3.9、定义只读字段
如果需要创建一个运行前不知道初始值且不可改变的字段时需要用到,比如创建一个类类型的实例。
使用关键字readonly。
与常量数据不同的另一个地方是:它们的值
可以在构造函数的作用于内被指定。当要赋给只读字段的值必须从外部源读取时是很有用的。
class Emplyee
{
public readonly string SSN;
public Employee(string empSNN)
{
SSN = empSSN;
}
}
只读字段并不是静态的。
3.10、static关键字
1、静态方法
静态方法只能操作类的静态成员。
2、静态数据
只分配一次,在所有对象实例之间共享。非静态方法也可以访问修改静态数据。
3、静态构造函数
用来对静态数据初始化。
class SavingAccount
{
...
static SavingAccount()
{
Console.WriteLine("In static ctor!");
currInterestRate = 0.04;
}
}
几个特性:
Ⅰ、
一个给定的类(结构)只能定义一个静态构造函数。
Ⅱ、
静态构造函数仅执行一次,与创建了多少这种类型的对象无关。
Ⅲ、
静态构造函数不带访问修饰符,也不带任何参数。
Ⅳ、
当静态构造函数创建这个类的实例时,或者在调用者访问第一个静态成员之前,运行库会调用静态构造函数
Ⅴ、
静态构造函数在任何实例级别的构造函数之前执行。
4、静态类
C#2.0新增。
只能包含静态方法和静态数据。不能用new创建。
c#2.0之前,防止创建这类数据的方法一是重定义默认构造函数为私有或用abstract将类标记为抽象类型,但是不是类型安全的。
3.11、方法参数修饰符
表3.4 C#参数修饰符
参数修饰符 |
作用 |
无 |
默认为按值传递(pass by value),意味着被调用的方法收到原始数据的一份副本。 |
out |
输出参数是由被调用的方法赋值的,因此是按引用传递(pass by reference)的,如果被调用的方法没有给输出参数赋值,会出现编译错误。 |
params |
该参数允许将一组可变个数的相同类型的参数作为单独的逻辑参数进行传递。方法只能有一个params修饰符,而且必须是方法的最后一个参数。 |
ref |
调用者赋初值,且可以由被调用的方法可选的重新复制,数据是按引用传递的。 |
1、默认的参数传递
按值传递,被调用者对参数的改变不会影响到调用者。
2、out修饰符
调用者和被调用者都需要添加out关键字。
如果调用者在调用被调用者之前对变量进行了赋值,那么该值在调用后将会消失。
一个显而易见的好处就是使用out参数可以让调用者只使用一次方法调用就能获得多个返回值。
3、ref修饰符
如果希望方法可以对在调用者作用域中声明的不同数据进行操作,就必须使用引用参数。
输出参数和引用参数有一些区别:
Ⅰ、
输出参数不需要在它们被传递给方法之前初始化。
Ⅱ、
引用参数必须在它们被传递给方法之前进行初始化
。
4、params修饰符
它可以用来创建一个方法,接受一组具有相同类型的参数,但作为一个
逻辑参数。
// params keyword.
// Return average of ‘some number’ of doubles.
static double CalculateAverage(params double[] values)
{
double sum = 0;
for (int i = 0; i < values.Length; i++)
sum += values[i];
return (sum / values.Length);
}
public static void
{
// Use 'params' keyword.
// Pass in a comma delimited list of doubles…
double average;
average = CalculateAverage(4.0, 3.2, 5.7);
Console.WriteLine("Average of 4.0, 3.2, 5.7 is: {0}", average);
// …or pass an array of doubles.
double[] data = { 4.0, 3.2, 5.7 };
average = CalculateAverage(data);
Console.WriteLine("Average of data is: {0}", average);
Console.ReadLine();
}
3.12、迭代结构
- for循环
- foreach/in循环
- while和do/while循环
3.13、判断结构与关系/相等运算符
只能使用bool型的表达式。
3.14、值类型和引用类型
值类型包括数
值类型、
枚举和
结构,它们都分配在
栈上,一旦离开定义的作用域,立即就会从内存中删除。
当一个值类型赋值给另一个值类型的时候,默认情况下完成的是一个成员到另一个成员的
复制。就数值和布尔型而言,唯一要复制的就是变量本身的值。
值类型都继承自System.ValueType。功能上说,System.ValueTpe类唯一作用就是重写由System.Object定义的虚方法,以遵守基于值而非引用的语义。
结构也是值类型,具有了在栈上分配数据效率的同时,又能发挥面向对象的最基本优点(封装)。
结构类型也是用new创建,但是是在栈上分配。
结构的默认构造函数是保留的,不允许重定义。
MyPoint p = new MyPoint();
也可以不用new,但是要对每一个字段赋值。
MyPoint p1;
p1.x = 100;
p1.y = 200;
1、值类型、引用类型和赋值运算符
值类型赋值的时候,是复制各个值到赋值目标,实际上各自在栈中都有存在,对一个的操作不会影响另一个。
引用类型赋值时,将会产生一个对该堆上同一个对象的新引用。
示例代码见“
2、包含引用类型的值类型”。
2、包含引用类型的值类型
当值类型包含其他引用参数时,赋值将生产一个“引用”的副本。这样就有了两个独立的结构,每一个都包含指向内存中同一个对象的引用,称为“浅复制”。
如果想执行一个“深复制”,即将内部引用的状态完全复制到一个新对象中时,需要实现ICloneable接口。
// Change struct to class to see the different behaviors.
struct MyPoint
{
public int x, y;
}
// This type will be used
// within a struct.
class ShapeInfo
{
public string infoString;
public ShapeInfo(string info)
{ infoString = info; }
}
// This stuct has members that
// are value types and ref types.
struct MyRectangle
{
public ShapeInfo rectInfo; // Ref type.
public int top, left, bottom, right;
public MyRectangle(string info)
{
rectInfo = new ShapeInfo(info);
top = left = 10;
bottom = right = 100;
}
}
class ValRefClass
{
static void Main(string[] args)
{
// The 'new' keyword is optional when creating value types
// using the default constructor, however you must assign
// all field data before use.
Console.WriteLine("***** Value Types / Reference Types *****");
// Still on the stack!
MyPoint p = new MyPoint();
Console.WriteLine("-> Creating p1");
MyPoint p1 = new MyPoint();
p1.x = 100;
p1.y = 100;
Console.WriteLine("-> Assigning p2 to p1");
MyPoint p2 = p1;
// Here is p1.
Console.WriteLine("p1.x = {0}", p1.x);//100
Console.WriteLine("p1.y = {0}", p1.y);//100
// Here is p2.
Console.WriteLine("p2.x = {0}", p2.x);//100
Console.WriteLine("p2.y = {0}", p2.y);//100
// Change p2.x. This will NOT change p1.x.
Console.WriteLine("-> Changing p2.x to 900");
p2.x = 900;
// Print again.
Console.WriteLine("-> Here are the X values again...");
Console.WriteLine("p1.x = {0}", p1.x);//100。如果将MyPoint改成类类型,此处会显示900。
Console.WriteLine("p2.x = {0}", p2.x);//900
Console.WriteLine();
// Create the first MyRectangle.
Console.WriteLine("-> Creating r1");
MyRectangle r1 = new MyRectangle("This is my first rect");
// Now assign a new MyRectangle to r1.
Console.WriteLine("-> Assigning r2 to r1");
MyRectangle r2;
r2 = r1;
// Change values of r2.
Console.WriteLine("-> Changing all values of r2");
r2.rectInfo.infoString = "This is new info!";
r2.bottom = 4444;
// Print values
Console.WriteLine("-> Values after change:");
Console.WriteLine("-> r1.rectInfo.infoString: {0}", r1.rectInfo.infoString);//This is new info!
Console.WriteLine("-> r2.rectInfo.infoString: {0}", r2.rectInfo.infoString);//This is new info!
Console.WriteLine("-> r1.bottom: {0}", r1.bottom);//100
Console.WriteLine("-> r2.bottom: {0}", r2.bottom);//4444
Console.ReadLine();
}
}
3、按值传递引用类型
传递的复制的一份指向调用者对象的引用。因此可以改变对象的域数据,但是重新赋值对调用者不可见。
示例代码请见“
4、
按引用传递引用类型”。
4、按引用传递引用类型
完全是对同一个对象操作,被调用者可以改变对象的状态数据的值和所引用的对象。可重新赋值。
// Simple class to demo params keyword.
class Person
{
public string fullName;
public int age;
public Person(string n, int a)
{
fullName = n;
age = a;
}
public void PrintInfo()
{
Console.WriteLine("{0} is {1} years old", fullName, age);
}
}
class Program
{
public static void ArrayOfObjects(params object[] list)
{
for (int i = 0; i < list.Length; i++)
{
if (list[i] is Person)
{
((Person)list[i]).PrintInfo();
}
else
Console.WriteLine(list[i]);
}
Console.WriteLine();
}
public static void SendAPersonByValue(Person p)
{
// Change some data of 'p'.
p.age = 99;
// This will be forgotten after the call!
p = new Person("Nikki", 999);
}
public static void SendAPersonByReference(ref Person p)
{
// Change some data of 'p'.
p.age = 555;
// 'p' is now reassigned!
p = new Person("Nikki", 999);
}
public static void Main()
{
// Passing ref-types by value.
Console.WriteLine("***** Passing Person object by value *****");
Person fred = new Person("Fred", 12);
Console.WriteLine("Before by value call, Person is:");
fred.PrintInfo();
SendAPersonByValue(fred);
Console.WriteLine("After by value call, Person is:");
fred.PrintInfo();//年龄的更改会有效果,但是重新赋值不会起效。
// Passing ref-types by ref.
Console.WriteLine("/n***** Passing Person object by reference *****");
Person mel = new Person("Mel", 23);
Console.WriteLine("Before by ref call, Person is:");
mel.PrintInfo();
SendAPersonByReference(ref mel);
Console.WriteLine("After by ref call, Person is:");
mel.PrintInfo();//被重新赋予另外一个对象
Console.ReadLine();
}
}
5、一些细节
表3.5 值类型和引用类型的比较
问题 |
值类型 |
应用类型 |
该类型分配在哪里? |
栈上 |
托管堆上 |
变量是如何表示的? |
值类型变量是局部复制 |
引用类型变量指向被分配的实例所占用的内存 |
基类型是什么? |
System.ValueType |
除System.ValueType之外的任何类型,只要那个类型不是密封的 |
该类型能作为其他类型的基类吗? |
不能。值类型总是密封的 |
是的。如果该类型不是密封的话 |
默认的参数传递行为是什么? |
变量是按值传递的,一个变量的副本被传入被调用的函数 |
变量按引用传递的,变量的地址传入被调用的参数 |
该类型能重写System.Object.Finalize()吗? |
不能。值类型不会放在堆上,因此不需要被终结 |
可以间接的重写 |
可以定义构造函数吗? |
是的。但是默认的构造函数要被保留 |
当然 |
该类型的变量什么时候消亡? |
当它们越出定义的作用域时 |
当托管堆被垃圾回收时 |
3.15、装箱与拆箱操作
装箱:显式的通过在System.Object中保存变量来将值类型转换成对应的引用类型的过程。当装箱一个值时,CLR在堆上分配一个新的对象,并将这个值类型的值复制到那个实例中。返回的是一个新分配对象的引用。
拆箱:将对象引用所保存的值转换成对应的栈上的值类型的过程。要先验证接受的数据类型与装箱的类型是否相同。
拆箱到一个合适的数据类型是强制的。
1、装箱和拆箱的用处
事实上很少需要手工的装箱和拆箱。大多数的时候,C#的编译器会在适当的时候自动装箱变量。例如传递参数的时候。
自动装箱也发生在.NET基类库中的类型的时候。例如System.Collections命名空间中的一些类类型。
装箱和拆箱会花费一定的时间。性能损失可以通过使用泛型来补偿。
2、拆箱自定义的值类型
也就是结构体和枚举的时候,先通过is关键字判断下是否为某种指定的类型。
3.16、使用.NET枚举
默认时计数方案将第一个元素设置为0,以后依次递增。可以改变第一个值,以下的依次递增。也可以指定全部的或部分的值,指定时并不必遵循有序的原则,只是在未指定的那里才将它的值设置成上一个的值加1。
默认枚举每一个项目的存储类型映射到System.Int32。可以改变这个设置。例如:
enmu EmpType : byte
{
Manager = 10;
Grunt = 1;
Contractor = 100;
VP = 9;
}
可以用枚举来代替“魔数”。
一个枚举的值必须总是带着含有前缀的枚举名称被应用,例如EmpType.Grunt而不是Grunt。
枚举都隐式的派生自System.Enum。该基类定义了很多静态方法。
表3.6 部分System.Enum静态成员
成员 |
作用 |
Format() |
根据指定的格式将指定的枚举类型的值转换成和它等价的字符串表示 |
GetName()/GetNames() |
在指定的、具有指定值的枚举中获取常量名称(或一个包含全部名称的数组) |
GetUnderlyingType() |
返回用来保存给定枚举值的底层数据类型 |
GetValues() |
在指定的枚举中获取常量值的数组 |
IsDefined() |
返回一个值指示值的常量是否存在于指定的枚举中 |
Parse() |
将一个或多个枚举常量的名称或数值的字符串表示转换成一个等价的枚举对象,返回的是System.Object,因此需要强制转换。 |
C#枚举支持各种运算符的使用。
// Here is a custom enum.
enum EmpType : byte
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VP = 9
}
class Program
{
#region Helper function
// Enums as parameters.
public static void AskForBonus(EmpType e)
{
switch (e)
{
case EmpType.Contractor:
Console.WriteLine("You already get enough cash...");
break;
case EmpType.Grunt:
Console.WriteLine("You have got to be kidding...");
break;
case EmpType.Manager:
Console.WriteLine("How about stock options instead?");
break;
case EmpType.VP:
Console.WriteLine("VERY GOOD, Sir!");
break;
default: break;
}
}
#endregion
static void Main(string[] args)
{
Console.WriteLine("***** Enums as parameters *****");
EmpType fred;
fred = EmpType.VP;
AskForBonus(fred);
// Print out string version of ‘fred’.
Console.WriteLine("/n***** ToString() *****");
Console.WriteLine(fred.ToString());
//Get underlying type.
Console.WriteLine("/n***** Enum.GetUnderlyingType() *****");
Console.WriteLine(Enum.GetUnderlyingType(typeof(EmpType)));
// Get Fred's type, hex and value.
Console.WriteLine("/n***** Enum.Format() *****");
Console.WriteLine("You are a {0}", fred.ToString());
Console.WriteLine("Hex value is {0}", Enum.Format(typeof(EmpType), fred, "x"));
Console.WriteLine("Int value is {0}", Enum.Format(typeof(EmpType), fred, "D"));
// Parse.
Console.WriteLine("/n***** Enum.Parse() *****");
EmpType sally = (EmpType)Enum.Parse(typeof(EmpType), "Manager");
Console.WriteLine("Sally is a {0}", sally.ToString());
// Get all stats for EmpType.
Console.WriteLine("/n***** Enum.GetValues() *****");
Array obj = Enum.GetValues(typeof(EmpType));
Console.WriteLine("This enum has {0} members:", obj.Length);
// Now show the string name and associated value.
foreach (EmpType e in obj)
{
Console.Write("String name: {0}", Enum.Format(typeof(EmpType), e, "G"));
Console.Write(" ({0})", Enum.Format(typeof(EmpType), e, "D"));
Console.Write(" hex: {0}/n", Enum.Format(typeof(EmpType), e, "X"));
}
// Does EmpType have a SalePerson value?
Console.WriteLine("/n***** Enum.IsDefined() *****");
if (Enum.IsDefined(typeof(EmpType), "SalesPerson"))
Console.WriteLine("Yep, we have sales people.");
else
Console.WriteLine("No, we have no profits....");
Console.WriteLine("/n***** < and > *****");
EmpType Joe = EmpType.VP;
EmpType Fran = EmpType.Grunt;
if (Joe < Fran)
Console.WriteLine("Joe's value is less than Fran's value.");
else
Console.WriteLine("Fran's value is less than Joe's value.");
Console.ReadLine();
}
}
3.17、最重要的类:System.Object
当定义一个不显式指定其基类的类时,它隐含着继承自System.Object。
System.Object定义了一组实例级别和类级别(静态)成员。其中一些实例级别的成员是用virtual关键字声明的,因此可以被派生类重写。
表3.7 System.Object的核心成员
Object类的实例方法 |
作用 |
Equals() |
默认情况下,这个方法仅当被比较的项是内存中的同一个项时才返回true。因此,该方法用来比较对象的引用,而不是对象的状态。典型情况下,这个方法重写为仅当被比较的对象拥有相同的内部状态值时返回ture(基于值的语义)。 注意:如果重写了Equals(),也应该重写GetHashCode()。 |
GetHashCode() |
这个方法返回一个能够标识内存中指定对象的整数(hash值)。 如果你打算将自定的类型包含进System.Collections.Hashtable类型中,强烈建议您重写这个成员的默认实现。 |
GetType() |
这个方法返回一个全面描述当前项细节的System.Type对象。简而言之,这是一个对所有对象都可用的运行时类型信息(RTTI)方法。 |
ToString() |
这个方法以那么生怕测。typename的格式(也就是完全限定名)返回一个给定对象的字符串表示。如果该类型不是定义在一个命名空间中,只返回typename。该方法也可以被子类重写,返回表示对象内部状态的“名称/值”对的标记化字符串,而不是对象的完全像定名。 |
Finalize() |
暂时可以将这个受保护的方法理解为,当一个对象从堆中被删除的时候由.NET运行库调用。 |
MemberwiseClone() |
这个受保护的方法返回一个新的对象,它是当前对象的逐个成员的副本。因此,如果你的对象包含到其他对象的引用,那么到这些类型的引用将被复制(也就是,它实现了浅复制)。如果对象包含值类型,得到的是值的完全副本。 |
3.18、重写System.Object的一些默认行为
示例代码省略。
System.Object定义了两个静态成员Object.Equals()和Object.ReferenceEquals()来测试基于值和基于引用的相等性。
示例代码待加
3.19、系统数据类型(和C#简化符号)
内建的C#数据类型实际上是一种简化符号,用来定义System命名空间中已定义的类型。
表3.8 系统类型和C#简化符号
C#简化符号 |
判断符合CLS |
系统类型 |
范围 |
作用 |
sbyte |
否 |
System.SByte |
-128~127 |
带符号的8位数 |
byte |
是 |
System.Byte |
0~255 |
无符号的8位数 |
short |
是 |
System.Int16 |
-32 768~32 767 |
带符号的16位数 |
ushort |
否 |
System.UInt16 |
0~65535 |
无符号的16位数 |
int |
是 |
System.Int32 |
-2 147 483 648~2 147 483 647 |
带符号的32位数 |
uint |
否 |
System.UInt32 |
0~4 294 967 295 |
无符号的32位数 |
long |
是 |
System.Int64 |
-9 223 372 036 854 775 808~ 9 223 372 036 854 775 807 |
带符号的64位数 |
ulong |
否 |
System.UInt64 |
0~18 446 744 073 709 551 615 |
无符号的64位数 |
char |
是 |
System.Char |
U0000~Uffff |
一个16位的unicode字符 |
float |
是 |
System.Single |
1.5×10-45~3.4×1038
|
32位浮点数 |
double |
是 |
System.Double |
5.0×10-324~1.7×10308
|
64位浮点数 |
bool |
是 |
System.Boolean |
true或false |
表示布尔值 |
decimal |
是 |
System.Decimal |
100~1028
|
96位带符号数 |
string |
是 |
System.String |
受系统内存限制 |
表示一个unicode字符集合 |
object |
是 |
System.Object |
任何类型都可以保存在一个object变量中 |
.NET世界中所有类型的基类 |
默认情况下,赋值运算符右边的一个实数值字面量被当作double类型.因此,为了初始化一个浮点变量,使用后缀f或F.
由于它们都是继承自System.Object,因此诸如GetHashCode等都可使用。
由于所有值类型都提供了一个默认的构造函数,因此使用new关键字创建一个系统类型是允许的,它将变量的值设置为默认值。
bool b1 = new bool(); //b1 = false
bool b2 = false;
.NET数值类型支持MaxValue和MinValue。可能还有其他更有用的成员,例如System.Double有Epsilon、PositiveInfinity、NegativeInfinity等成员。
1、Boolean、Char、DateTime和TimeSpan类型
System.Boolean不支持MaxValue和MinValue属性,但是支持TrueString和FalseString属性。
System.Char和String类型一样,也是基于unicode的。
.NET数据类型有这样一种能力,通过给定文本生成(解析)相应的底层类型的变量。
bool myBool = bool.Parse("True");
System命名空间提定义了一些很有用的数据类型,但没有对应的C#关键字,具体说就是DateTime和TimeSpan结构。
3.20、System.String数据类型
一些基本的操作:Length、Contains()、Format()、Insert()、PadLeft()、PadRight()、Remove()、Replace()、SubString()、ToCharArray()、ToUpper()、ToLower()。
尽管string是一个引用类型,但是相等性运算符(==和!=)被定义为比较字符串对象的值而不是它们所引用的内存。
string类型可以使用类似数组的语法遍历每一个字符,就是其内容支持数组式访问的对象使用了索引器方法。
也可以使用foreach来遍历,因为System.String维护着一个System.Char类型的数组。如下:
foreach(char c in sl)
Console.WriteLine(c);
1、转义字符
支持同C语言基本一致的转义字符,如下:/'、/"、//、/a、/n、/r、/t。
C#引入了以@为前缀的字符串字面量记法,称为“逐字字符串”。例如:
Console.WriteLine(@"C:/MyApp/bin/debug");
还可以容国重复“标记向一个字面量字符串插入一个双引号”,例如:
Console.WriteLine(@"Cerbus said ""Darrr!""");
3.21、System.Text.StringBuilder的作用
由于string一旦建立,它的值就不能再修改,因此造成处理大文本数据时效率的低下,而StringBuilder提供了对底层缓冲区的直接访问,因此可以适应。默认StringBuilder的容量是16。
在很多情况下,应该用System.String来表示文本,但是如果是一个文本密集的应用程序,应该用StingBuilder。
3.22、.NET数组类型
数据是引用类型,继承自System.Array的公共基类,默认情况下,数组以0为下界,但是也可以使用静态的System.Array.CreateInstance()方法来创建一个带有任意下界的数组。
如果创建一个值在以后才被确定的数组,应在它分配的时候使用方括号指定数组的大小。例如:
string[] booksOnCOM;
booksOnCOM = new string[3];
以上也可以用一行代码来表示:
string[] booksOnCOM = new string[3];
如果在声明时就知道数组的值,可以在大括号中指定这些值。这种情况下,数组的
大小和
new关键字都是
可选的。例如:
int[] n = new int[] {1, 2, 3 4};
int[] n3 = {1, 2, 3, 4};
最后还有一种方式如下:
int[] n2 = new int[4] {1, 2, 3, 4};
这种情况下,指定的数值代表数组中元素的个数,而不是上界的值。如果声明的大小和初始化的个数不一致将会导致编译错误。
不管怎样声明一个数组,都要知道.NET数组中的元素会自动设置为它们各自的默认值。
1、数组作为参数和返回值
数组可以作为参数传递,也可以当作成员返回值接收。
2、多维数组
第一种为矩阵数组,例如:
int[,] myMatrix;
myMartix = new int[6,6];
第二种称为交错数组,它由一些内部的数组组成,其中的每一组都可能有不同的上界。
int[][] myJagArray = new int[5]][];
for(int i = 0; i < myJaqArray.Length; i++)
myJaqArray[i] = new int[i+7];
3、 System.Array基类
表3.9 部分System.Array成员
成员 |
作用 |
BinarySearch() |
这个静态方法在(已经排序的)数组中搜索指定的项。如果这个数组是由你创建的自定义类型组成的,该类型必须实现IComparer接口才能进行二分搜索。 |
Clear() |
这个静态方法将数组中一个范围内的元素设定为空值(值类型为0,引用类型为null)。 |
CopyTo() |
用来从源数组向目标数组复制元素。 |
Length |
只读属性用来确定一个数组中元素的数目。 |
Rank |
该属性返回数组的维数。 |
Reverse() |
这个静态方法反转一个一维数组的内容。 |
Sort() |
这个方法给一个内建类型的一维数组排序。如果数组中的元素实现了IComparer接口,也可以给自定义类型排序。 |
3.23、可空类型
CLR数据类型有个一确定的范围,例如Boolean型只能从集合{true, false}中赋值,但是还可以创建可空类型,这样就可以赋给null值,这样就可以从集合{true, false, null}中赋值了。
为了定义一个可空类型,应在底层数据类型中添加问号符号作为后缀。例如:
int? nullableInt = 10;
值得注意的是:这种语法只对值类型或者值类型的数组是合法的。
??运算符:是C#2005特有的。这个运算符在获得的值实际上是null时给一个可空类型赋值。例如:
int? myData = dr.GetIntFromDB() ?? 100;
3.24、定义自定义命名空间
略去