C#1.0
和 Visual Studio .NET 2002
一起发布的 C# 版本 1.0
非常像 Java
。 在 ECMA 制定的设计目标中,它旨在成为一种“简单、现代、面向对象的常规用途语言”。 当时,它和 Java
类似,说明已经实现了上述早期设计目标。
C# 1.0 的主要功能包括:
- 类
- 结构
- 接口
- 属性
- 委托
- 事件
- 表达式
- 语句
- 特性
基元类型
编译器直接支持的数据类型称为基元类型(primitive type).
基元类型直接映射到到Framework类库(FCL)中存在的类型。例如C#的int
直接映射到System.Int32
类型。
C#类型关键字 | FCL类型 | 符合公共语言规范(CLS) | 说明 |
---|---|---|---|
sbyte | System.SByte | 否 | 有符号8位值 |
byte | System.Byte | 是 | 无符号8位值 |
short | System.Int16 | 是 | 有符号16位值 |
ushort | System.UInt16 | 否 | 无符号16位值 |
int | System.Int32 | 是 | 有符号32位值 |
uint | System.UInt32 | 否 | 无符号32位值 |
long | System.Int64 | 是 | 有符号64位值 |
unlong | System.UInt64 | 否 | 无符号64位值 |
char | System.Char | 是 | 16位Unicode字符 |
float | System.Single | 是 | IEEE32位浮点值 |
double | System.Double | 是 | IEEE64位浮点值 |
bool | System.Boolean | 是 | 真或者假 |
decimal | System.Decimal | 是 | 128位高精度浮点值,常用于不容许舍入误差的金融计算。 |
string | System.String | 是 | 字符数组,引用类型 |
object | System.Object | 是 | 所有类型的基类,引用类型 |
dynamic(4.0新增) | System.Object | 是 | 对于CLR,dynamic和object完全一致。但C#编译器允许使用简单的语法让dynamic变量参与动态调度。 |
CLR支持两种类型:引用类型和值类型。
类
使用class
定义的类型一般叫做类,是引用类型。 在运行时,如果声明了引用类型的变量,那变量的值就一直是 null
,直到使用 new
运算符显式创建类实例,或直到为这个变量分配可能已在其他位置创建的兼容类型的对象。
类的声明
使用class
关键字可以声明类。
class
关键字前面是访问修饰符,从低到高分别是:
- private(私有的,只能在类的内部访问)
- protected internal(当前项目中受保护的,只能在同一个程序集下类的内部和继承类中访问)
- protected(受保护的,只能在内部和继承类中访问)
- internal(内部的,只能在同一个程序集中访问)
- public(公共的,访问不受限制)
类名称必须是有效的 C# 标识符名称。 定义的其余部分是类的主体,其中定义了行为和数据。 类上的字段、属性、方法和事件统称为类成员。
类的实例
使用new
运算符来创建类的实例,类的实例一般称为对象。
创建对象时,new
运算符所做的事情:
- 计算类型及其所有基类型(一直到
System.Objec
)中定义的所有实例字段需要的字节数。托管堆上每个对象都需要一些额外的成语,包括类型对象指针(type object pointer)
和同步块索引(sync block index)
。CLR利用这些成语管理对象。额外成语的字节数要计入对象大小。 - 从托管堆中分配类型要求的字节数,从而分配对象的内存,分配的所有字节都设为零。
- 初始化对象的
类型对象指针
和同步块索引
成员。 - 调用类型的实例构造函数。每个类型的构造器都负责初始化该类型定义的实例字段。最终调用到
System.Objec
的构造函数。
new
执行了所有这些操作之后,返回指向新建对象的一个引用(或指针)。
类的继承
在 C# 中仅允许单一继承。 也就是说,一个类仅能从一个基类继承实现。 但是,一个类可实现多个接口。
CLR要求所有类型最终都从System.Object类型派生。
结构
结构类型(“structure type
”或“struct type
”)是一种可封装数据和相关功能的值类型 。 由于结构是值类型,所以它的实例一般在线程栈上分配。对于托管堆上的类型,在分配内存和 CLR 自动内存管理功能(称为“垃圾回收”)回收内存时都会产生开销,而值类型的实例不受垃圾回收器的控制,因此值类型的使用缓解了托管堆的压力,并减少了应用程序生存期内的垃圾回收次数。
结构的初始化
使用struct
关键字声明结构。
在C#中,必须先初始化已声明的变量,然后才能使用该变量。 由于结构类型变量不能为 null(除非它是可为空的值类型的变量),因此,必须实例化相应类型的实例。 有多种方法可实现此目的。
- 通常,可使用 new 运算符调用适当的构造函数来实例化结构类型。 每个结构类型都至少有一个构造函数。 这是一个隐式无参数构造函数,用于生成类型的默认值。 还可以使用默认值表达式来生成类型的默认值。
struct MyStruct
{
public int x;
}
public static void Main()
{
MyStruct m = new MyStruct();
Console.WriteLine(m.x);
}
- 如果结构类型的所有实例字段都是可访问的,则还可以在不使用 new 运算符的情况下对其进行实例化。 在这种情况下,在首次使用实例之前必须初始化所有实例字段。
struct MyStruct
{
public int x;
}
public static void Main()
{
MyStruct m;
m.x = 1;
Console.WriteLine(m.x);
}
结构的限制
设计结构类型是,具有与类类型相同的功能,但有以下例外:
- 不能声明无参构造杉树。每个结构类型都已经提供了一个隐式无参构造函数,该构造函数生成类型的默认值。
- 不能在声明实例字段或属性时对它们进行初始化。 但是,可以在其声明中初始化静态或常量字段或静态属性。
- 结构类型的构造函数必须初始化该类型的所有实例字段。
- 结构类型不能从其他类或结构类型继承,也不能作为类的基础类型。 但是,结构类型可以实现接口。
- 不能在结构类型中声明终结器。
接口
接口包含非抽象类或结构必须实现的一组相关功能的定义。 接口可以定义 static 方法,此类方法必须具有实现。
使用 interface
关键字定义接口,接口名称必须是有效的 C# 标识符名称。 按照约定,接口名称以大写字母 I
开头。
接口的成员有方法
、属性
、事件
、索引器
。默认情况下,接口成员是公共的。
interface IInterface
{
static void StaticMethod()
{
Console.WriteLine("hello world!");
}
void Mehod();
string Property { get; }
event EventHandler<string> MyEvent;
string this[int i] { get; }
}
属性
属性语法是字段的自然延伸。属性定义包含 get
和 set
访问器的声明,这两个访问器用于检索该属性的值以及对其赋值。
访问属性时,其行为类似于字段。 但与字段不同的是,属性通过访问器实现;访问器用于定义访问属性或为属性赋值时执行的语句。
private int myProperty;
public int MyProperty
{
get
{
return myProperty;
}
set
{
myProperty = value;
}
}
// 自动属性
public int MyProperty { get; set; }
委托
委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。
.NET Framework通过委托来提供回调函数机制。不同于其他平台的回调机制,委托的功能要多得多。
- 委托确保回调方法是类型安全的
- 委托允许顺序调用多个方法
- 委托支持调用静态方法和实例方法
使用类似于定义方法签名的语法来定义委托类型。 只需向定义添加 delegate
关键字即可。
// 定义一个无返回值的委托类型,它的实例引用一个方法。
// 该方法获取一个string参数,返回void
// 实际上编译器会帮我们生成一个完整的类,编译器定义的类有4个方法:一个构造器、Invoke、BeginInvoke、EndInvoke。
public delegate void HelloDelegate(string name);
// 定义委托之后,可以创建该类型的实例。
// 每个委托对象实际都是一个包装器,其中包装了一个方法和调用该方法时要操作的对象。
HelloDelegate helloDelegate = new HelloDelegate(Console.WriteLine);
// 调用委托
// 实际上等同于 helloDelegate.Invoke("Zhangsan");
helloDelegate("Zhangsan");
// 为了方便C#开发人员,C#编译器自动为委托类型的实例重载了 += 和 -=操作符。
// 这些操作符分别调用 Delegate.Combine 和 Delegate.Remove。可以用这些操作符简化委托链的构造。
HelloDelegate writeDelegate = new HelloDelegate(Console.Write);
// 简写:helloDelegate += writeDelegate;
helloDelegate = (HelloDelegate)Delegate.Combine(helloDelegate, writeDelegate);
// 简写:helloDelegate -= writeDelegate;
helloDelegate = (HelloDelegate)Delegate.Remove(helloDelegate, writeDelegate);
事件
和委托类似,事件是后期绑定机制。 实际上,事件是建立在对委托的语言支持之上的。
事件是对象用于(向系统中的所有相关组件)广播已发生事情的一种方式。 任何其他组件都可以订阅事件,并在事件引发时得到通知。
使用event
关键字声明事件。
public event EventHandler<FileListArgs> Progress;
使用委托调用语法调用事件处理程序。
Progress.Invoke(this, new FileListArgs(file));
订阅及取消事件
EventHandler<FileListArgs> onProgress = (sender, eventArgs) =>
Console.WriteLine(eventArgs.FoundFile);
fileLister.Progress += onProgress;
fileLister.Progress -= onProgress;
表达式
表达式是由一个或多个操作数以及零个或多个运算符组成的序列,其计算结果为一个值、对象、方法或命名空间。
表达式可以包含文本值、方法调用、运算符及其操作数,或简单名称 。 简单名称可以是变量名、类型成员名、方法参数名、命名空间名或类型名。
表达式可以使用运算符(运算符又可使用其他表达式作为参数)或方法调用(方法调用的参数又可以是其他方法调用),因此表达式可以非常简单,也可以极其复杂。
运算符
在表达式中,运算符优先级和结合性决定了操作的执行顺序。 可以使用括号更改由运算符优先级和结合性决定的计算顺序。
当运算符的优先级相同,运算符的结合性决定了运算的执行顺序:
- 左结合运算符按从左到右的顺序计算。 除
赋值运算符
和null 合并运算符
外,所有二元运算符都是左结合运算符。 例如,a + b - c 将计算为 (a + b) - c。 - 右结合运算符按从右到左的顺序计算。
赋值运算符
、null 合并运算符
和条件运算符?:
是右结合运算符。 例如,x = y = z 将计算为 x = (y = z)。
下表按最高优先级到最低优先级的顺序列出 C# 运算符(目前最新版本的所有运算符,不特指C#1.0版本中的运算符)。 每行中运算符的优先级相同。
运算符 | 类别或名称 |
---|---|
x.y、f(x)、a[i]、x?.y、x?[y]、x++、x--、x!、new、typeof、checked、unchecked、default、nameof、delegate、sizeof、stackalloc、x->y | 基本 |
+x、-x、!x、~x、++x、--x、^x、(T)x、await、&x、*x、true 和 false | 一元运算符 |
x..y | 范围 |
switch | switch表达式 |
x * y、x / y、x % y | 乘除 |
x + y、x – y | 加减 |
x << y、x >> y | 移位 |
x < y、x > y、x <= y、x >= y、is、as | 关系和类型测试 |
x == y、x != y | 相等 |
x & y | 布尔逻辑 AND 或按位逻辑 AND |
x ^ y | 布尔逻辑 XOR 或按位逻辑 XOR |
x | y | 布尔逻辑 OR 或按位逻辑 OR |
x && y | 条件“与” |
x || y | 条件“或” |
x ?? y | Null 合并运算符 |
c ? t : f | 条件运算符 |
x = y、x += y、x -= y、x *= y、x /= y、x %= y、x &= y、x |= y、x ^= y、x <<= y、x >>= y、x ??= y、=> | 赋值和 lambda 声明 |
语句
程序执行的操作采用语句表达。 常见操作包括声明变量、赋值、调用方法、循环访问集合,以及根据给定条件分支到一个或另一个代码块。 语句在程序中的执行顺序称为“控制流”或“执行流”。 根据程序对运行时所收到的输入的响应,在程序每次运行时控制流可能有所不同。
语句可以是以分号结尾的单行代码,也可以是语句块中的一系列单行语句。 语句块括在括号 {} 中,并且可以包含嵌套块。
语句的类型
下表列出了 C# 中的各种语句类型及其关联的关键字和说明
类别 | C#关键字/说明 |
---|---|
声明语句 | 声明语句引入新的变量或常量。 变量声明可以选择为变量赋值。 在常量声明中必须赋值。 |
表达式语句 | 用于计算值的表达式语句必须在变量中存储该值。 |
选择语句 | 选择语句用于根据一个或多个指定条件分支到不同的代码段。例如:if 、else 、switch 、case
|
迭代语句 | 迭代语句用于遍历集合,或重复执行同一组语句直到满足指定的条件。例如:for 、foreach 、while 、do-while
|
跳转语句 | 跳转语句将控制转移给另一代码段。例如:break 、continue 、return 、goto 、yield
|
异常处理 | 异常处理语句用于从运行时发生的异常情况正常恢复。例如:throw 、try-catch-finally
|
Checked 和 unchecked | 用于指定将结果存储在变量中、但该变量过小而不能容纳结果值时,是否允许数值运算导致溢出。 |
await 语句 | 如果用 async 修饰符标记方法,则可以使用该方法中的 await 运算符。 在控制到达异步方法的 await 表达式时,控制将返回到调用方,该方法中的进程将挂起,直到等待的任务完成为止。 任务完成后,可以在方法中恢复执行。 |
yield return 语句 | 迭代器对集合执行自定义迭代,如列表或数组。 迭代器使用 yield return 语句返回元素,每次返回一个。 到达 yield return 语句时,会记住当前在代码中的位置。 下次调用迭代器时,将从该位置重新开始执行。 |
fixed 语句 | fixed 语句禁止垃圾回收器重定位可移动的变量。 |
lock 语句 | lock 语句用于限制一次仅允许一个线程访问代码块。 |
带标签的语句 | 可以为语句指定一个标签,然后使用 goto 关键字跳转到该带标签的语句。 |
空语句 | 空语句只含一个分号。 不执行任何操作,可以在需要语句但不需要执行任何操作的地方使用。 |
特性
Attribute
使用特性,可以有效地将元数据或声明性信息与代码(程序集、类型、方法、属性等)相关联。 将特性与程序实体相关联后,可以在运行时使用反射这项技术查询特性。
特性具有以下属性:
- 特性向程序添加元数据。 元数据是程序中定义的类型的相关信息。 所有 .NET 程序集都包含一组指定的元数据,用于描述程序集中定义的类型和类型成员。 可以添加自定义特性来指定所需的其他任何信息。
- 可以将一个或多个特性应用于整个程序集、模块或较小的程序元素(如类和属性)。
- 特性可以像方法和属性一样接受自变量。
- 程序可使用反射来检查自己的元数据或其他程序中的元数据。