本书中几乎所有的程序和代码片段都可以作为交互式示例在 LINQPad 中运行。阅读本书时使用这些示例可以加快你的学习进度。在 LINQPad 中编辑执行这些示例可以立即得到结果,无须在 VisualStudio 中建立项目和解决方案。
在 C# 中,语句按顺序执行,每个语句都以分号结尾。类将函数成员和数据成员聚合在一起形成面向对象的构建单元。Console 类将处理命令行的输入输出功能聚合在一起,例如 WriteLine 方法。类是一种类型,我们会在 2.3 节进行介绍。
可以使用 using 指令导入命名空间来避免烦冗的代码:
using System;
int x = 12 * 30;
Console.WriteLine(x);
一系列语句被成对的大括号包围起来,称为语句块(statement block)。
方法是 C# 中的诸多种类函数之一。另一种函数是我们用来执行乘法运算的 * 运算符。其他的函数种类还包括构造器、属性、事件、索引器和终结器。
编译
C# 编译器能够将一系列 .cs 为扩展名的源代码文件编译成程序集,程序集是 .NET 中工单打包和部署单元。程序集可以是一个应用程序也可以是一个库。
普通的控制台程序或 Windows 应用程序包含一个入口点(entry point),而库则没有。库可以被应用程序或其他的库调用(引用)。.NET 5 就是由一系列库(及运行时环境)组成的。
上一节中的每一个程序都是直接由一系列语句(称为顶级语句)开头的。当存在顶级语句时,控制台程序或 Windows 应用程序将隐式创建入口点(若没有顶级语句,则 Main 方法将作为应用程序的入口点——请参见 2.3.2 节)
与 .NET Framework 不同,.NET 6 程序集并没有 .exe 扩展名。.NET 6 应用程序构建之后生成的 .exe 文件只是一个负责启动 .dll 程序集的原生加载器。这个 .exe 文件是和平台相关的。
.NET 5 能够创建自包含部署程序,它包含加载器、程序集以及 .NET 运行时本身。而以上内容均包含在一个单一 .exe 文件中。
dotnet 工具(在 Windows 下则为 dotnet.exe)是一个用于管理 .NET 源代码和二进制文件的命令行工具。该工具可以像集成开发环境(例如 VisualStudio 和 Visual Studio Code)那样构建或启动程序。
dotnet 工具可通过安装 .NET 5 SDK 或安装 Visual Studio获得,其默认安装位置在 Windows 操作系统上位于 %ProgramFiles%/dotnet,在 UbuntuLinux 上位于 /usr/bin/dotnet。
dotnet 工具在编译应用程序时需要指定一个工程文件(project file)及一个或者多个 C# 代码文件。以下命令将创建一个控制台应用程序的基本结构:
dotnet new Console -n MyFirstProgram
上述命令将创建名为 MyFirstProgram 的子目录,并在其中创建名为 MyFirstProgram.csproj 的工程文件,以及包含 Main 方法的 Program.cs 代码文件,其中 Main 方法将在控制台输出“Hello World”。
在 MyFirstProgram 目录执行以下命令将构建并启动上述应用程序:
dotnet run MyFirstProgram
如果仅仅希望构建应用程序,但不执行,则可以执行以下命令:
dotnet build MyfirstProgram.csproj
构建生成的程序集将保存在 bin/debug 子目录下。
我们将在第 17 章详细介绍程序集。
C# 的语法基于 C 和 C++ 语法。
标识符是程序员为类、方法、变量等选择的名字。
C# 标识符是区分大小写的,通常约定参数、局部变量以及私有字段应该以小写字母开头(例如 myVariable),而其他类型的标识符则应该以大写字母开头(例如 MyMethod)。
字面量在语法上是嵌入程序中的原始数据片段。
C#提供了两种不同形式的源代码文档:单行注释和多行注释。多行注释由/*
开始,由*/
结束。
本书的大多数代码需要使用 System 命名空间下的类型。因此除了展示与命名空间相关的概念,后面的示例我们将忽略“using System”语句。
变量表示一个存储位置,其中的值可能会不断变化。与之对应,常量总是表示同一个值。C#中的所有值都是某一种类型的实例。
预定义类型是指那些由编译器特别支持的类型,例如 int。
在 C# 中,预定义类型(也称为内置类型)拥有相应的C#关键字。在 .NET 的 System 命名空间下也包含了很多不是预定义类型的重要类型(例如 DateTime)。
类型包含数据成员和函数成员。
C# 的优点之一是其中的预定义类型和自定义类型非常相近。
构造器的定义类似于方法,不同的是它的方法名和返回类型是合并在一起的,并且其名称为所属的类型名称。
默认情况下,成员就是实例成员。
不对类型实例进行操作的数据成员和函数成员可以标记为 static(静态)。
事实上,Console 类是一个静态类,即它的所有成员都是静态的,并且该类型无法实例化。
public class Panda
{
public string Name; // Instance field
public static int Population; // Static field
public Panda(string n)
{
Name = n;
Population = Population + 1;
}
}
如果试图求p1.Population或者Panda.Name的值,则会产生编译时错误。
public 关键字将成员公开给其他类,如果字段没有标记为公有(public)的,那么它就是私有的。
命名空间是组织类型的有效手段,对于大型程序尤为如此。
到目前为止,本书的范例均使用了顶级语句(顶级语句是C# 9引入的特性)。
如不使用顶级语句,C# 将查找静态 Main 方法,并将这个方法作为程序入口点。Main 方法可以定义在任何类中(并且只能够存在一个 Main 方法)。如果 Main 方法需要访问特定类型的私有成员,则可以将 Main 方法定义在相应类中。这种做法要比顶级语句更简单。
Main 方法可以返回一个整数(而非 void)。该整数将返回到执行环境中(一般非零值代表失败)。Main 方法也可以接受一个字符串数组作为参数(该数组将包含所有传递给可执行程序的参数)。
Main方法也可以声明为async方法,并返回Task或者Task以支持异步编程。我们将在第14章介绍该内容。
顶级语句(C#9)
C# 9 引入的顶级语句可以避免静态 Main 方法及包含该方法的类型。具备顶级语句的文件由以下三部分组成:
例如:
using System; // Part 1
Console.WriteLine("Hello, world"); // Part 2
void SomeMethod1() { } // Part 3
Console.WriteLine("Hello again!"); // Part 4
void SomeMethod2() { } // Part 5
class SomeClass { } // Part 6
namespace SomeNamespace { } // Part 7
由于 CLR 并不显式支持顶级语句,因此编译器会将上述代码转换为类似以下形式:
using System; // Part 1
static class Program$ // Special compiler-generated name
{
static void Main$ (string[] args)
{
Console.WriteLine("Hello, world"); // Part 2
void SomeMethod1() { } // Part 3
Console.WriteLine("Hello again!"); // Part 4
void SomeMethod2() { } // Part 5
}
}
class SomeClass { } // Part 6
namespace SomeNamespace { } // Part 7
请注意,第 2 部分(Part 2)是包裹在主方法中的。这意味着 SomeMethod1 和 SomeMethod2 都是局部方法。我们将会在 3.1.3.2 节中进行完整介绍。而目前最重要的是局部方法(非 static 的声明)可以访问声明在父级方法中的变量:
int x = 3;
LocalMethod();
void LocalMehtod() { Console.WriteLine(x); } // we can access x
这种方式的其他后果就是顶级方法无法从其他类或类型中访问。
顶级语句可以将整数返回给调用者(并非必需),并可以“神奇地”访问 string[] 类型的 args 参数,以对应调用者从命令行中传递给程序的参数。
由于每一个应用程序只可能拥有一个入口,因此在 C# 项目中最多只能在一个文件里使用顶级语句。
转换始终会根据一个已经存在的值创建一个新的值。
隐式转换只有在以下条件都满足时才能进行:
相对地,只有在满足下列条件时才需要显式转换:
如果编译器可以确定某个转换必定失败,那么这两种转换都无法执行。包含泛型的转换在特定情况下也会失败,请参见 3.9.11 节。
以上的数值转换是 C# 中内置的。C# 还支持引用转换、装箱转换(参见第 3 章)与自定义转换(参见 4.17 节)。对于自定义转换,编译器并没有强制满足上述规则,因此没有良好设计的类型有可能在转换时产生意想不到的效果。
C# 中的类型可以分为以下几类:
本节将介绍值类型和引用类型。泛型参数将在 3.9 节介绍,指针类型将在 4.18 节中介绍。
值类型包含大多数的内置类型(具体包括所有数值类型、char 类型和 bool 类型)以及自定义的 struct 类型和 enum 类型。
引用类型包含所有的类、数组、委托和接口类型,其中包括了预定义的string类型。
值类型和引用类型最根本的不同在于它们在内存中的处理方式。
值类型的变量或常量的内容仅仅是一个值。
可以通过struct关键字定义自定义值类型。
public struct Point
{
public int X;
public int Y;
}
值类型实例的赋值总是会进行实例复制,例如:
Point p1 = new Point();
p1.X = 7;
Point p2 = p1; // Assignment causes copy
Console.WriteLine(p1.X); // 7
Console.WriteLine(p2.X); // 7
p1.X = 9; // Change p1.X
Console.WriteLine(p1.X); // 9
Console.WriteLine(p2.X); // 7
引用类型比值类型复杂,它由对象和对象引用两部分组成。
给引用类型变量赋值只会复制引用,而不是对象实例。
引用可以用字面量null来赋值,表示它并不指向任何对象。
从技术上说,CLR 用整数倍字段的大小(最大到 8 字节)来分配内存地址。因此,下面定义的对象实际上会占用 16 字节的内存(第一个字段的 7 个字节被“浪费了”):
struct A
{
byte b;
long l;
}
这种行为可以通过指定 StructLayout 特性来重写(请参见 24.6 节)。
引用类型需要为引用和对象单独分配存储空间。管理开销的精确值本质上属于 .NET 运行时实现的细节,但最少也需要8字节来存储该对象类型的键、一些诸如线程锁的状态,以及是否可以被垃圾回收器固定等临时信息。根据 .NET 运行时工作的平台类型(32 位或 64 位平台),每一个对象的引用都需要额外的 4 字节或 8 字节的存储空间。
C# 中的预定义类型有:
值类型
引用类型
C# 的预定义类型或称为 .NET 类型,均位于 System 命名空间下。因而以下两个语句仅在拼写上有所不同:
int i = 5;
System.Int32 i = 5;
在 CLR 中,除了 decimal 之外的一系列预定义值类型属于基元类型。之所以将其称为基元类型是因为它们在编译过的代码中有直接的指令支持,而这种指令通常转换为底层处理器直接支持的指令,例如:
int i = 7; // 0x7
bool b = true; // 0x1
char c = 'A'; // 0x41
float f = 0.5f; // uses IEEE floating-point encoding
System.IntPtr 以及 System.UIntPtr 类型也是基元类型(参见第24章)。
nint 和 unint 是 C# 9 引入的原生大小的整数类型,它们适用于执行指针算法,我们将在 4.18.8 节进行介绍。
从 .NET 5 开始,运行时引入了一种 16 位浮点类型,称为 Half。该类型主要针对与显卡处理器的互操作,大多数 CPU 都没有对这种类型提供原生的支持。Half 并非 CLR 的基元类型,C# 对该类型也不存在特殊的语言支持。
十六进制辅以 0x 前缀,十六进制辅以 0x 前缀。
略。
略。
略。
略。
略。
略。
略。
略。
略。
默认情况下,溢出会默默地发生而不会抛出任何异常,且其溢出行为是“周而复始”的。
checked运算符的作用是,在运行时当整数类型表达式或语句超过相应类型的算术限制时不再默默地溢出,而是抛出 OverflowException。checked 运算符可在有++、–、+、-(一元运算符和二元运算符)、*、/和整数类型间显式转换运算符的表达式中起作用。溢出检查会带来微小的性能损失。
checked 运算符对 double 和 float 类型没有作用(它们会溢出为特殊的“无限”值,这会在后面介绍),对 decimal 类型也没有作用(这种类型总是会进行溢出检查)。
checked 运算符既可以包裹表达式也能够包裹语句块,例如:
int a = 1000000;
int b = 1000000;
int c = checked(a * b); // checks just the expression
checked
{
c = a * b;
}
在编译时打开 checked 开关(在 Visual Studio 中,可以在“Advanced Build Settings”中设置)将使程序在默认情况下对所有表达式都进行算术溢出检查。如果你只想禁用指定表达式或语句的溢出检查,可以用 unchecked 运算符。
int x = int.MaxValue;
int y = unchecked(x + 1);
unchecked { int z = x + 1; }
无论是否打开了 checked 工程选项,编译时的表达式计算总会检查溢出,除非使用 unchecked 运算符。