本书中几乎所有的程序和代码片段都可以作为交互式示例在 LINQPad 中运行。阅读本书时使用这些示例可以加快你的学习进度。在 LINQPad 中编辑执行这些示例可以立即得到结果,无须在 VisualStudio 中建立项目和解决方案。
2.1 第一个 C# 程序
在 C# 中,语句按顺序执行,每个语句都以分号结尾。类将函数成员和数据成员聚合在一起形成面向对象的构建单元。Console 类将处理命令行的输入输出功能聚合在一起,例如 WriteLine 方法。类是一种类型,我们会在 2.3 节进行介绍。
可以使用 using 指令导入命名空间来避免烦冗的代码:
c#
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# 代码文件。以下命令将创建一个控制台应用程序的基本结构:
cmd
dotnet new Console -n MyFirstProgram
上述命令将创建名为 MyFirstProgram 的子目录,并在其中创建名为 MyFirstProgram.csproj 的工程文件,以及包含 Main 方法的 Program.cs 代码文件,其中 Main 方法将在控制台输出"Hello World"。
在 MyFirstProgram 目录执行以下命令将构建并启动上述应用程序:
cmd
dotnet run MyFirstProgram
如果仅仅希望构建应用程序,但不执行,则可以执行以下命令:
cmd
dotnet build MyfirstProgram.csproj
构建生成的程序集将保存在 bin/debug 子目录下。
我们将在第 17 章详细介绍程序集。
2.2 语法
C# 的语法基于 C 和 C++ 语法。
2.2.1 标识符和关键字
标识符是程序员为类、方法、变量等选择的名字。
C# 标识符是区分大小写的,通常约定参数、局部变量以及私有字段应该以小写字母开头(例如 myVariable),而其他类型的标识符则应该以大写字母开头(例如 MyMethod)。
2.2.2 字面量、标点与运算符
字面量在语法上是嵌入程序中的原始数据片段。
2.2.3 注释
C#提供了两种不同形式的源代码文档:单行注释和多行注释。多行注释由/*
开始,由*/
结束。
2.3 类型基础
本书的大多数代码需要使用 System 命名空间下的类型。因此除了展示与命名空间相关的概念,后面的示例我们将忽略"using System"语句。
变量表示一个存储位置,其中的值可能会不断变化。与之对应,常量总是表示同一个值。C#中的所有值都是某一种类型的实例。
2.3.1 预定义类型示例
预定义类型是指那些由编译器特别支持的类型,例如 int。
在 C# 中,预定义类型(也称为内置类型)拥有相应的C#关键字。在 .NET 的 System 命名空间下也包含了很多不是预定义类型的重要类型(例如 DateTime)。
2.3.2 自定义类型(Class)
2.3.2.1 类型成员
类型包含数据成员和函数成员。
2.3.2.2 预定义类型和自定义类型
C# 的优点之一是其中的预定义类型和自定义类型非常相近。
2.3.2.3 构造器和实例化
构造器的定义类似于方法,不同的是它的方法名和返回类型是合并在一起的,并且其名称为所属的类型名称。
2.3.2.4 实例与静态成员
默认情况下,成员就是实例成员。
不对类型实例进行操作的数据成员和函数成员可以标记为 static(静态)。
事实上,Console 类是一个静态类,即它的所有成员都是静态的,并且该类型无法实例化。
c#
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的值,则会产生编译时错误。
2.3.2.5 public 关键字
public 关键字将成员公开给其他类,如果字段没有标记为公有(public)的,那么它就是私有的。
2.3.2.6 定义命名空间
命名空间是组织类型的有效手段,对于大型程序尤为如此。
2.3.2.7 定义 Main 方法
到目前为止,本书的范例均使用了顶级语句(顶级语句是C# 9引入的特性)。
如不使用顶级语句,C# 将查找静态 Main 方法,并将这个方法作为程序入口点。Main 方法可以定义在任何类中(并且只能够存在一个 Main 方法)。如果 Main 方法需要访问特定类型的私有成员,则可以将 Main 方法定义在相应类中。这种做法要比顶级语句更简单。
Main 方法可以返回一个整数(而非 void)。该整数将返回到执行环境中(一般非零值代表失败)。Main 方法也可以接受一个字符串数组作为参数(该数组将包含所有传递给可执行程序的参数)。
Main方法也可以声明为async方法,并返回Task或者Task以支持异步编程。我们将在第14章介绍该内容。
顶级语句(C#9)
C# 9 引入的顶级语句可以避免静态 Main 方法及包含该方法的类型。具备顶级语句的文件由以下三部分组成:
- using 指令(可选)
- 一系列语句,其中也可以包含方法的声明
- 类型与命名空间声明(可选)
例如:
c#
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 并不显式支持顶级语句,因此编译器会将上述代码转换为类似以下形式:
c#
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 的声明)可以访问声明在父级方法中的变量:
c#
int x = 3;
LocalMethod();
void LocalMehtod() { Console.WriteLine(x); } // we can access x
这种方式的其他后果就是顶级方法无法从其他类或类型中访问。
顶级语句可以将整数返回给调用者(并非必需),并可以"神奇地"访问 string[] 类型的 args 参数,以对应调用者从命令行中传递给程序的参数。
由于每一个应用程序只可能拥有一个入口,因此在 C# 项目中最多只能在一个文件里使用顶级语句。
2.3.3 类型和转换
转换始终会根据一个已经存在的值创建一个新的值。
隐式转换只有在以下条件都满足时才能进行:
- 编译器确保转换总能成功。
- 没有信息在转换过程中丢失。
相对地,只有在满足下列条件时才需要显式转换:
- 编译器不能保证转换总是成功。
- 信息在转换过程中有可能丢失。
如果编译器可以确定某个转换必定失败,那么这两种转换都无法执行。包含泛型的转换在特定情况下也会失败,请参见 3.9.11 节。
以上的数值转换是 C# 中内置的。C# 还支持引用转换、装箱转换(参见第 3 章)与自定义转换(参见 4.17 节)。对于自定义转换,编译器并没有强制满足上述规则,因此没有良好设计的类型有可能在转换时产生意想不到的效果。