3. C# 语言基础
3.1. 简介
我们将首先将 C# 视为一种经典的编程语言。关于类的内容我们稍后再讨论。一个程序包含两部分:
- 数据
- 处理这些数据的指令
我们通常会尝试将数据与指令分离:
![]() |
3.2. C# 数据
C# 使用以下数据类型:
- 整数
- 实数
- 十进制数
- 字符和字符串
- 布尔值
- 对象
3.2.1. 预定义数据类型
C# 类型 | .NET 类型 | 表示的数据 | 后缀 字面值 | 编码 | 值域 |
字符 (S) | 字符 | 2字节 | Unicode字符(UTF-16) | ||
字符串 (C) | 字符串 | 对一串 Unicode 字符的引用 | |||
Int32 (S) | 整数 | 4字节 | [-2³¹ , 2³¹-1] [-2147483648, 2147483647] | ||
UInt32 (S) | .. | U | 4 字节 | [0, 2³²-1] [0, 4294967295] | |
Int64 (S) | .. | L | 8 字节 | [-263, 263 -1] [-9223372036854775808, 9223372036854775807] | |
UInt64 (S) | .. | UL | 8 字节 | [0, 264 -1] [0, 18446744073709551615] | |
.. | 1 字节 | [-27, 27 -1] [-128, +127] | |||
字节 (S) | .. | 1 字节 | [0, 28 -1] [0,255] | ||
Int16 (S) | .. | 2字节 | [-215, 215-1] [-32768, 32767] | ||
UInt16 (S) | .. | 2字节 | [0, 2¹⁶-1] [0,65535] | ||
单精度 (S) | 实数 | F | 4字节 | [1.5 × 10⁻⁴⁵, 3.4 × 10³⁸(绝对值) | |
双精度 (S) | .. | D | 8 字节 | [-1.7 × 10³⁰⁸, 1.7 × 10³⁰⁸(绝对值) | |
十进制 (S) | 十进制数 | M | 16 字节 | [1.0 10⁻²⁸, 7.9 10²⁸] 绝对值,有效数字为 28 位 | |
布尔 (S) | .. | 1 字节 | true, false | ||
对象 (C) | 对象引用 | 对象引用 |
上文中的 C# 类型均以其对应的 .NET 类型表示,若类型为结构体则标注 (S),若类型为类则标注 (C)。我们发现 32 位整数有两种可能的类型:int 和 Int32。其中 int 类型是 C# 类型。 Int32 是属于 System 名称空间的结构体,其全名为 System.Int32。类型 int 是 C# 对 .NET 结构体 System.Int32 的别名。同样地,C# 中的 string 是 .NET 类型 System.String 的别名。System.String 是一个类,而非结构体。这两个概念虽相似,但存在以下根本区别:
- 结构类型(Structure)的变量可通过其值进行操作
- 类类型的变量可通过其地址(在面向对象语言中称为引用)进行操作。
结构和类都是带有属性和方法的复杂类型。例如,我们可以编写:
上文中的数字 3 在 C# 中默认是 int 类型,即 .NET 中的 System.Int32。该结构拥有一个 GetType() 方法,该方法会返回一个封装了 System.Int32 数据类型特征的对象。其中包括 FullName 属性,该属性返回该类型的完整名称。如您所见,数字 3 比乍看之下要复杂得多。
以下是一个说明这些要点的程序:
using System;
namespace Chap1 {
class P00 {
static void Main(string[] args) {
// example 1
int ent = 2;
float fl = 10.5F;
double d = -4.6;
string s = "essai";
uint ui = 5;
long l = 1000;
ulong ul = 1001;
byte octet = 5;
short sh = -4;
ushort ush = 10;
decimal dec = 10.67M;
bool b = true;
Console.WriteLine("Type de ent[{1}] : [{0},{2}]", ent.GetType().FullName, ent,sizeof(int));
Console.WriteLine("Type de fl[{1}]: [{0},{2}]", fl.GetType().FullName, fl, sizeof(float));
Console.WriteLine("Type de d[{1}] : [{0},{2}]", d.GetType().FullName, d, sizeof(double));
Console.WriteLine("Type de s[{1}] : [{0}]", s.GetType().FullName, s);
Console.WriteLine("Type de ui[{1}] : [{0},{2}]", ui.GetType().FullName, ui, sizeof(uint));
Console.WriteLine("Type de l[{1}] : [{0},{2}]", l.GetType().FullName, l, sizeof(long));
Console.WriteLine("Type de ul[{1}] : [{0},{2}]", ul.GetType().FullName, ul, sizeof(ulong));
Console.WriteLine("Type de b[{1}] : [{0},{2}]", octet.GetType().FullName, octet, sizeof(byte));
Console.WriteLine("Type de sh[{1}] : [{0},{2}]", sh.GetType().FullName, sh, sizeof(short));
Console.WriteLine("Type de ush[{1}] : [{0},{2}]", ush.GetType().FullName, ush, sizeof(ushort));
Console.WriteLine("Type de dec[{1}] : [{0},{2}]", dec.GetType().FullName, dec, sizeof(decimal));
Console.WriteLine("Type de b[{1}] : [{0},{2}]", b.GetType().FullName, b, sizeof(bool));
}
}
}
- 第 7 行:声明一个整型变量 ent
- 第 19 行:变量 v 的类型可通过 v.GetType().FullName 获取。S 结构的大小可通过 sizeof(S) 获取。指令 Console.WriteLine("... {0} ... {1} ...", param0, param1, ...) 会在屏幕上输出其第一个参数的文本,并将每个 {i} 标记替换为表达式 parami 的值。
- 第 22 行:无法使用 string 运算符,因为它指定的是类而非结构体 sizeof。
结果如下:
显示结果为 .NET 类型,而非 C# 别名。
3.2.2. 字面量数据的表示法
145、-7、0xFF(十六进制) | |
100000L | |
134.789, -45E-18 (-45 × 10⁻¹⁸) | |
134.789F, -45E-18F (-45 × 10⁻¹⁸) | |
100000M | |
'A', 'b' | |
"today" "c:\chap1\paragraph3" @"c:\chap1\paragraph3" | |
true, false | |
new DateTime(1954,10,13) (年, 月, 日) 表示 1954年10月13日 |
请注意以下两个字面字符串:"c:\chap1\\paragraph3" 和 @"c:\chap1\paragraph3"。在字面字符串中,\ 字符会被解释。因此,"\n" 代表行尾标记,而不是 \ 和 n 这两个字符的连续组合。如果我们想要这个连续组合,就必须写成 "\\n",这样该序列才会被解释为单个 \ 字符。 你也可以写 @"\n" 来获得相同的结果。语法 @"text" 要求将文本完全按原样处理。这有时被称为字符串原样引用。
3.2.3. 数据报告
3.2.3.1. 声明的作用
程序操作的数据由名称和类型来定义。这些数据存储在内存中。当程序被编译时,编译器会为每个数据项分配一个由地址和大小定义的内存位置。它通过程序员编写的声明来完成这一操作。
声明还能帮助编译器检测编程错误。例如,
x=x*2;
如果 x 是字符串,则会被判定为错误。
3.2.3.2. 常量的声明
常量的声明语法如下:
例如:
为什么要声明常量?
- 如果给常量起一个有意义的名称,程序会更易于阅读:
- 如果“常量”发生变化,修改程序会更加容易。例如,在前面的例子中,如果增值税税率变为 33%,唯一需要修改的就是定义其值的语句:
如果程序中显式使用了0.186,则将不得不修改许多语句。
3.2.3.3. 变量声明
变量通过名称来标识,并指向一种数据类型。C# 区分大小写。因此 FIN 和 end 是不同的。
变量可以在声明时进行初始化。声明一个或多个变量的语法如下:
其中类型标识符可以是预定义类型,也可以是程序员定义的类型。此外,变量在声明时也可以进行初始化。
您还可以通过使用关键字 var 代替 类型标识符 来避免指定变量的确切类型:
关键字 var 并不意味着变量没有特定类型。变量 variablei 被赋予了数据类型 valuei。此处初始化是强制要求的,以便编译器能够推导出变量的类型。
以下是一个示例:
using System;
namespace Chap1 {
class P00 {
static void Main(string[] args) {
int i=2;
Console.WriteLine("Type de int i=2 : {0},{1}",i.GetType().Name,i.GetType().FullName);
var j = 3;
Console.WriteLine("Type de var j=3 : {0},{1}", j.GetType().Name, j.GetType().FullName);
var aujourdhui = DateTime.Now;
Console.WriteLine("Type de var aujourdhui : {0},{1}", aujourdhui.GetType().Name, aujourdhui.GetType().FullName);
}
}
}
- 第 6 行:显式类型的数据
- 第 7 行:(data).GetType().Name 是 (data) 的短名称,(data).GetType().FullName 是 (data) 的全名
- 第 8 行:隐式类型数据。由于 3 的类型为 int,因此 j 的类型也将是 int。
- 第 10 行:隐式类型数据。由于 DateTime.Now 的类型是 DateTime,因此 today 的类型也将是 DateTime。
执行后,我们得到以下结果:
通过关键字 var 隐式声明的变量无法更改类型。例如,在代码第 10 行之后,该行:
var aujourdhui = "aujourd'hui";
我们稍后将看到,可以在表达式中“动态”声明类型。在此情况下,它是一个匿名类型,即用户未为其命名的类型。编译器会为该新类型命名。如果要将匿名类型赋值给变量,声明它的唯一方法是使用关键字 var。
3.2.4. 数字与字符串之间的转换
nombre.ToString() | |
int.Parse(string) 或 System.Int32.Parse | |
long.Parse(string) 或 System.Int64.Parse | |
double.Parse(string) 或 System.Double.Parse(string) | |
float.Parse(string) 或 System.Float.Parse(string) |
如果字符串不表示一个有效的数字,将字符串转换为数字可能会失败。 引发名为 exception 的致命错误。可以通过以下 try/catch 语句处理此错误:
try{
appel de la fonction susceptible de générer l'exception
} catch (Exception e){
traiter l'exception e
}
instruction suivante
如果函数没有抛出异常,我们就执行下一条指令;否则,我们就进入 catch 子句的主体,然后执行下一条指令。我们稍后会再讨论异常处理。下面是一个演示数字与字符串之间转换技巧的程序。在这个示例中,函数 poster 会将其参数的值输出到屏幕上。因此,poster(S) 会将 S 的值输出到屏幕上,其中 S 的类型为字符串。
using System;
namespace Chap1 {
class P01 {
static void Main(string[] args) {
// data
const int i = 10;
const long l = 100000;
const float f = 45.78F;
double d = -14.98;
// number --> string
affiche(i.ToString());
affiche(l.ToString());
affiche(f.ToString());
affiche(d.ToString());
//boolean --> string
const bool b = false;
affiche(b.ToString());
// string --> int
int i1;
i1 = int.Parse("10");
affiche(i1.ToString());
try {
i1 = int.Parse("10.67");
affiche(i1.ToString());
} catch (Exception e) {
affiche("Erreur : " + e.Message);
}
// chain --> long
long l1;
l1 = long.Parse("100");
affiche(l1.ToString());
try {
l1 = long.Parse("10.675");
affiche(l1.ToString());
} catch (Exception e) {
affiche("Erreur : " + e.Message);
}
// chain --> double
double d1;
d1 = double.Parse("100,87");
affiche(d1.ToString());
try {
d1 = double.Parse("abcd");
affiche(d1.ToString());
} catch (Exception e) {
affiche("Erreur : " + e.Message);
}
// string --> float
float f1;
f1 = float.Parse("100,87");
affiche(f1.ToString());
try {
d1 = float.Parse("abcd");
affiche(f1.ToString());
} catch (Exception e) {
affiche("Erreur : " + e.Message);
}
}// fine hand
public static void affiche(string S) {
Console.Out.WriteLine("S={0}",S);
}
}// end of class
}
第 30-32 行处理可能发生的异常。e.Message 是与异常 e 关联的错误消息。
结果如下:
请注意,字符串形式的实数必须使用逗号,而不是小数点。因此,我们写成
但
3.2.5. 数据表
C# 数组是一种用于将同类型数据归类到同一标识符下的对象。其声明如下:
n 是数组可容纳的数据项数量。语法 Table[i] 表示第 i 个数据项,其中 i 属于区间 [0,n-1]。任何对数据 Table[i] 的引用,若 i 不属于区间 [0,n-1],都将引发异常。数组在声明时也可以进行初始化:
或者简写为:
数组有一个名为 Length 的属性,它表示数组中的元素个数。
二维数组可以声明如下:
Type[,] array = new Type[n, m];
其中 n 表示行数,m 表示列数。语法 Table[i,j] 表示表格第 i 行中的第 j 个元素。二维数组也可以在声明时同时进行初始化:
或者简写为:
每个维度的元素个数可通过 GetLength(i) 获取,其中 i=0 表示对应第一个索引的维度,i=1 表示对应第二个索引的维度,依此类推
维数总数可通过 Rank 属性获取,元素总数可通过 Length 获取。
表的表声明如下:
Type[][] array = new Type[n][];
上述语句创建了一个包含 n 行表的数组。每个元素 table[i] 都是一个一维数组引用。在上述声明中,这些引用 array[i] 并未被初始化。它们的值是引用 null。
以下示例演示了表数组的创建:
// un tableau de tableaux
string[][] noms = new string[3][];
for (int i = 0; i < noms.Length; i++) {
noms[i] = new string[i + 1];
}//for
// initialisation
for (int i = 0; i < noms.Length; i++) {
for (int j = 0; j < noms[i].Length; j++) {
noms[i][j] = "nom" + i + j;
}//for j
}//for i
- 第 2 行:一个名为 names 的 3 维字符串数组(string[][])。每个元素都是一个数组指针(对象引用),其元素类型为 string[]。
- 第3-5行:数组 names 的 3 个元素被初始化。现在每个元素都“指向”一个类型为 string[] 的数组。names[i][j] 即为由 names[i] 引用的第 j 个 string[] 数组。
- 第 9 行:在双重循环内初始化数组 names[i][j] 的元素。此处 names[i] 是一个包含 i+1 个元素的数组。由于 names[i] 是一个数组,因此 names[i].Length 表示其元素个数。
以下是关于我们刚刚介绍的三种表类型的示例:
using System;
namespace Chap1 {
// tables
using System;
// test class
public class P02 {
public static void Main() {
// an initialized 1-dimensional array
int[] entiers = new int[] { 0, 10, 20, 30 };
for (int i = 0; i < entiers.Length; i++) {
Console.Out.WriteLine("entiers[{0}]={1}", i, entiers[i]);
}//for
// an initialized 2-dimensional array
double[,] réels = new double[,] { { 0.5, 1.7 }, { 8.4, -6 } };
for (int i = 0; i < réels.GetLength(0); i++) {
for (int j = 0; j < réels.GetLength(1); j++) {
Console.Out.WriteLine("réels[{0},{1}]={2}", i, j, réels[i, j]);
}//for j
}//for i
// a table of tables
string[][] noms = new string[3][];
for (int i = 0; i < noms.Length; i++) {
noms[i] = new string[i + 1];
}//for
// initialization
for (int i = 0; i < noms.Length; i++) {
for (int j = 0; j < noms[i].Length; j++) {
noms[i][j] = "nom" + i + j;
}//for j
}//for i
// display
for (int i = 0; i < noms.Length; i++) {
for (int j = 0; j < noms[i].Length; j++) {
Console.Out.WriteLine("noms[{0}][{1}]={2}", i, j, noms[i][j]);
}//for j
}//for i
}//Main
}//class
}//namespace
执行后,我们得到以下结果:
3.3. C# 基础语法
区分
1 计算机执行的基本指令。
2 用于控制程序顺序的指令。
当你考察微计算机及其外围设备的结构时,基本指令的含义便会变得清晰。
![]() |
-
从键盘读取信息
-
信息处理
-
将信息写入屏幕
3.3.1. 在屏幕上显示
屏幕写入指令有多种:
Console.Out.WriteLine(expression)
Console.WriteLine(expression)
Console.Error.WriteLine (expression)
其中表达式可以是任何能够转换为字符串并在屏幕上显示的数据类型。所有 C# 或 .NET 对象都具有 ToString() 方法,用于执行此转换。
System.Console 类提供了屏幕写入操作(Write、WriteLine)。Console 类具有两个属性 Out 和 Error,它们是 TextWriter 类型的写入流:
- Console.WriteLine() 与 Console.Out.WriteLine() 效果相同,通常会写入与屏幕关联的 Out 流。
- Console.Error.WriteLine() 则写入 Error 流,该流通常与屏幕相关联。
如稍后将看到,Out 和 Error 流可在程序运行时重定向到文本文件。
3.3.2. 读取输入的数据
来自键盘的数据流由类型为 TextReader 的 Console.In 对象表示。此类对象可用于通过 ReadLine 方法读取一行文本:
Console 类提供了一个 ReadLine 方法,该方法默认与 In 相关联。因此我们可以这样编写:
键盘上输入的行会被存储在变量 ligne* 中,随后程序可以使用该变量。与 **Out** 和 Error 流一样,In* 流也可以重定向到文件。
3.3.3. 输入输出示例
以下是一个简短的程序,用于演示键盘/屏幕输入输出操作:
using System;
namespace Chap1 {
// test class
public class P03 {
public static void Main() {
// write to Out feed
object obj = new object();
Console.Out.WriteLine(obj);
// write to the Error stream
int i = 10;
Console.Error.WriteLine("i=" + i);
// reading a line entered on the keyboard
Console.Write("Tapez une ligne : ");
string ligne = Console.ReadLine();
Console.WriteLine("ligne={0}", ligne);
}//fine hand
}//end of class
}
- 第 9 行:obj 是一个对象引用
- 第 10 行:将 obj 输出到屏幕上。默认情况下,会调用 obj.ToString()。
- 第 14 行:你也可以这样写:
Console.Error.WriteLine("i={0}",i);
第一个参数 "i={0}" 是显示格式,其余参数则是待显示的表达式。{n} 元素是“位置”参数。在运行时,{n} 参数会被第 n 个表达式的值替换。
结果如下:
- 第 1 行:代码第 10 行生成的显示内容。obj.ToString() 显示了变量 obj 的类型名称:System.Object。类型 object 是 .NET System.Object 类型的 C# 别名。
3.3.4. I/O 重定向
在 DOS 和 UNIX 系统中,有三个被称为的标准设备:
- 标准输入设备——默认连接键盘,编号为 0
- 标准输出设备——默认指向显示器,编号为 1
- 标准错误设备——默认指向屏幕,编号为 2
在 C# 中,Console.Out 写入设备 1,写入流 Console.Error 写入设备 2,而读取流 Console.In 从设备 0 读取数据。
当您在 DOS 或 Unix 环境下运行程序时,可以设置执行程序将使用哪些设备(0、1 和 2)。请考虑以下命令行:
在 argi 程序 pg 的实现背后,标准 I/O 设备可以重定向到文件:
标准输入流 0 被重定向到 in.txt。因此,在程序中,Console.In 将从 in.txt 中获取数据。 | |
将第 1 号输出重定向到文件 out.txt。这意味着在程序中,Console.Out 会将数据写入 out.txt | |
同上,但写入的数据将追加到 out.txt 文件的现有内容中。 | |
将第 2 号输出重定向到文件 error.txt。这意味着在程序中,Console.Error 会将其数据写入 error.txt | |
同上,但写入的数据将追加到当前文件 error.txt 的内容中。 | |
设备 1 和 2 均被重定向至 |
请注意,要将程序的 I/O 流重定向到文件,无需修改程序本身。操作系统会自动识别外设 0、1 和 2 的类型。请看以下程序:
using System;
namespace Chap1 {
// redirections
public class P04 {
public static void Main(string[] args) {
// lecture flux In
string data = Console.In.ReadLine();
// write Out feed
Console.Out.WriteLine("écriture dans flux Out : " + data);
// écriture flux Error
Console.Error.WriteLine("écriture dans flux Error : " + data);
}//Main
}//class
}
让我们生成此源代码的可执行文件:
![]() |
- 在 [1] 中:通过右键单击项目并选择“构建”来创建可执行文件
- [2]:在 DOS 窗口中,可执行文件 04.exe 已生成在项目的 bin/Release 目录下。
现在在 DOS 窗口 [2] 中执行以下命令:
- 第 1 行:in.txt 文件中的字符串 test
- 第 2-3 行:显示 in.txt 文件内容以供验证
- 第 4 行:执行程序 04.exe。In 流重定向至 in.txt,Out 流重定向至 out.txt,Error 流重定向至 err.txt。执行过程中未产生任何输出。
- 第 5-6 行:out.txt 文件内容。该内容表明:
- in.txt 文件已被读取
- 屏幕输出已重定向至 out.txt
- 第 7-8 行:对 err.txt 文件进行类似检查
我们可以清楚地看到,Out 和 In 流并未写入相同的设备,因为它们已被分别重定向。
3.3.5. 将表达式的值赋给变量
这里我们关注的是操作 variable=expression;
表达式的类型可以是:算术、关系、布尔、字符
3.3.5.1. 赋值运算的解释
运算 variable=expression;
本身是一个表达式,其求值过程如下:
- 先求值赋值语句的右侧:结果是一个 V 值。
- 将值 V 赋给变量
- 将值 V 赋给变量该 V 值同时也是赋值表达式的值,此时将其视为一个表达式。
这就是
是合法的。由于运算优先级,最右侧的 = 运算符将被求值。因此我们得到
表达式 V2=expression 被求值并赋值为 V。求值该表达式导致 V 被赋值给 V2。随后,接下来的 = 运算符被求值为:
该表达式的值仍然是 V。其求值导致 V 被赋值给 V1。
因此,V1=V2=表达式
是一个表达式,其求值
- 会将表达式的值赋给变量 V1 和 V2
- 将表达式的值赋给变量 V1 和 V2。
这可以推广为如下形式的表达式:
3.3.5.2. 算术表达式
算术表达式的运算符如下:
-
加法
-
减法
* 乘法
/ 除法:如果至少有一个操作数是实数,结果就是精确的商。如果两个操作数都是整数,结果就是整数商。因此 5/2 -> 2,而 5.0/2 -> 2.5。
% 除法:无论被除数和除数是什么,结果都是余数,商为整数。因此,这是一种模运算。
数学函数种类繁多。以下列举其中几种:
平方根 | |
余弦 | |
正弦 | |
正切 | |
x 的 y 次方 (x>0) | |
指数 | |
自然对数 | |
绝对值 |
等等...
所有这些函数都在一个名为 Math 的 C# 类中定义。使用时,必须在函数名前加上其所属类的名称。例如,编写:
Math类的完整定义如下:





3.3.5.3. 算术表达式的运算优先级
在求解算术表达式时,运算符的优先级如下(从高到低):
同一组 [ ] 内的运算符具有相同的优先级。
3.3.5.4. 关系表达式
运算符如下:
运算符优先级
若表达式为假,则关系表达式的结果为布尔值 false;否则为 true。
两个特征的比较
考虑两个字符 C1 和 C2。可以使用运算符对它们进行比较
随后会比较它们的 Unicode 码(即数字)。在 Unicode 排序中,它们之间存在以下关系:
比较两个字符串
它们按字符逐个比较。当两个字符之间首次出现不等关系时,即判定两个字符串之间存在相同含义的不等关系。
示例:
比较“Cat”和“Dog”这两个字符串
![]() |
这个最后的不等式意味着“猫” < “狗”。
或者比较“Cat”和“Kitten”这两个链。在“Cat”链耗尽之前,两者始终处于平局状态。在这种情况下,耗尽的链被宣布为“最小”的。因此,我们得到如下关系:
用于比较两个字符串的函数
您可以使用关系运算符 == 和 != 来测试两个字符串是否相等,或者使用 System.String 的 Equals 类。对于 <、<=、> 和 >= 关系,请使用 System.String 的 CompareTo 类:
using System;
namespace Chap1 {
class P05 {
static void Main(string[] args) {
string chaine1="chat", chaine2="chien";
int n = chaine1.CompareTo(chaine2);
bool egal = chaine1.Equals(chaine2);
Console.WriteLine("i={0}, egal={1}", n, egal);
Console.WriteLine("chien==chaine1:{0},chien!=chaine2:{1}", "chien"==chaine1,"chien" != chaine2);
}
}
}
第 7 行,变量 i 的值将为:
0 如果两个字符串相等
1 若通道 1 > 通道 2
-1 若链 1 < 链 2
第8行,如果两个字符串相等,变量egal的值为true,否则为false。第10行使用==和!=运算符来检查两个字符串是否相等。
执行结果:
3.3.5.5. 布尔表达式
可使用的运算符包括与 (&&)、或 (||) 和非 (!). 布尔表达式的结果是一个布尔值。
运算符优先级:
- !
- &&
- ||
double x = 3.5;
bool valide = x > 2 && x < 4;
关系运算符具有优先级更高的运算符 && 和 ||。
3.3.5.6. 位运算
运算符
设 i 和 j 是两个整数。
将 i 向左移 n 位。移入的位均为零。 | |
将 i 向右移位 n 位。如果 i 是带符号整数(signed char、int、long),则保留符号位。 | |
对 i 和 j 进行逐位逻辑与运算。 | |
对 i 和 j 进行逐位或运算。 | |
将 i 补为 1 | |
对 i 和 j 进行异或运算 |
或者以下代码:
short i = 100, j = -13;
ushort k = 0xF123;
Console.WriteLine("i=0x{0:x4}, j=0x{1:x4}, k=0x{2:x4}", i,j,k);
Console.WriteLine("i<<4=0x{0:x4}, i>>4=0x{1:x4},k>>4=0x{2:x4},i&j=0x{3:x4},i|j=0x{4:x4},~i=0x{5:x4},j<<2=0x{6:x4},j>>2=0x{7:x4}", i << 4, i >> 4, k >> 4, (short)(i & j), (short)(i | j), (short)(~i), (short)(j << 2), (short)(j >> 2));
- 格式 {0:x4} 以十六进制格式 (x) 显示第 0 个参数,并使用 4 个字符 (4)。
结果如下:
3.3.5.7. 运算符组合
a=a+b 可以写成 a+=b
a=a-b 可以写成 a-=b
运算符 /, %, *, <<, >>, &, |, ^ 也适用此规则。因此 a=a/2; 可以写成 a/=2;
3.3.5.8. 递增和递减运算符
变量++ 表示 variable=variable+1 或 variable+=1
将 -- 运算符应用于变量,即表示 variable = variable - 1,或者说 variable += -1
3.3.5.9. 三元运算符?
该表达式
的求值过程如下:
1 求值表达式 expr_cond。这是一个条件表达式,其条件为真或假
2 若为真,则该表达式的值为 expr1 的值,且 expr2 不被求值。
3 若为假,则情况相反:该表达式的值为 expr2 的值,且 expr1 不被求值。
运算 i=(j>4 ? j+1:j-1); 将被赋值给变量 i:若 j>4 则为 j+1,否则为 j-1。这等同于编写 if(j>4) i=j+1; else i=j-1;,但更为简洁。
3.3.5.10. 运算符的一般优先级
gd | |
dg | |
dg | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
dg | |
dg |
gd 表示在优先级相同时遵循从左到右的优先级规则。这意味着当表达式中存在优先级相同的运算符时,表达式中最左侧的运算符将首先被计算。dg 表示从右到左的优先级。
3.3.5.11. 类型转换
在表达式中,您可以临时更改值的编码。这被称为更改数据类型或类型转换。在表达式中更改值类型的语法如下:
此时,该值将采用指定的类型。这会导致该值的编码发生改变。
using System;
namespace Chap1 {
class P06 {
static void Main(string[] args) {
int i = 3, j = 4;
float f1=i/j;
float f2=(float)i/j;
Console.WriteLine("f1={0}, f2={1}",f1,f2);
}
}
}
- 第 7 行,f1 的值为 0.0。由于两个操作数均为 int 类型,因此 3/4 的除法是整数除法。
- 第8行,(float)i 表示将变量 i 的值转换为 float 类型。此时,我们进行的是 float 类型的实数与 int 类型的整数之间的除法运算,即实数之间的除法。变量 j 的值也会被转换为 float 类型,随后对这两个实数进行除法运算。因此,f2 的值将为 0.75。
以下是计算结果:
在 (float)i 中:
- i 是一个精确的 2 字节编码值
- (float) i 是作为真实的 4 字节近似值编码的相同值
i 的值。这种转码仅在计算期间发生,且变量 i 始终保持其 int 类型。
3.4. 程序流程控制指令
3.4.1. 停止
在“环境”中定义的 Exit 会停止程序执行。
syntaxe void Exit(int status)
action arrête le processus en cours et rend la valeur status au processus père
Exit 终止当前进程并将控制权交还给调用进程。调用进程可使用 status 的值。在 DOS 环境下,该 status 变量映射到系统变量 ERRORLEVEL,其值可在批处理文件中进行检测。在 Unix 环境下,使用 Bourne 命令解释器时,变量 $? 可获取 status 的值。
将以状态值 0 终止程序执行。
3.4.2. 简单选择结构
注:
- 条件用大括号括起来。
- 每个操作以分号结尾。
- 大括号不以分号结尾。
- 只有当存在多个操作时才需要使用大括号。
- else 子句可以省略。
- 没有 then 子句。
该结构的算法等价形式为 if .. then ... otherwise :
![]() |
示例
if (x>0) { nx=nx+1;sx=sx+x;} else dx=dx-x;
选择结构可以嵌套:
有时会出现以下问题:
using System;
namespace Chap1 {
class P07 {
static void Main(string[] args) {
int n = 5;
if (n > 1)
if (n > 6)
Console.Out.WriteLine(">6");
else Console.Out.WriteLine("<=6");
}
}
}
在上一个示例中,第 10 行的 else 指的是哪个 if 语句?规则是 else 总是指最近的 if 语句:在本例中即第 8 行的 if(n>6)。再来看另一个示例:
if (n2 > 1) {
if (n2 > 6) Console.Out.WriteLine(">6");
} else Console.Out.WriteLine("<=1");
这里我们希望在 if(n2>1) 之后添加 else 语句,而在 if(n2>6) 之后不添加 else 语句。鉴于前面的说明,我们必须在 if(n2>1) {...} else ... 之后添加大括号。
3.4.3. 案例结构
语法如下:
switch(expression) {
case v1:
actions1;
break;
case v2:
actions2;
break;
. .. .. .. .. ..
default:
actions_sinon;
break;
}
注释
- switch 的值可以是整数、字符或字符串
- 表达式 用方括号括起。
- 默认子句可以省略。
- 值 vi 是表达式的可能取值。如果表达式的值为 vi,则执行 case vi 子句后面的操作。
- break 语句将使我们退出 case 结构。
- 与值 vi 关联的每个指令块必须以分支指令(break、goto、return 等)结尾,否则编译器会报告错误。
示例
访问 算法
selon la valeur de choix
cas 0
fin du module
cas 1
exécuter module M1
cas 2
exécuter module M2
sinon
erreur<--vrai
findescas
在 C# 中
3.4.4. 循环结构
3.4.4.1. 已知的重复次数
用于
语法如下:
for (i=id;i<=if;i=i+ip){
actions;
}
注释
- for 语句的 3 个参数用圆括号括起,并用分号分隔。
- 每个 for 语句都以分号结尾。
- 只有当存在多个操作时才需要大括号。
- 大括号后不跟分号。
其算法等价形式为 for 循环:
这可以翻译为:
foreach 结构
语法如下:
foreach (Type variable in collection)
instructions;
}
注释
- collection 是一组可枚举对象。我们已知的可枚举对象集合是数组
- Type 是集合中对象的类型。对于数组而言,即为数组元素的类型
- 变量是循环内的局部变量,它将依次取集合中的所有值作为其值。
因此,以下代码:
string[] amis = { "paul", "hélène", "jacques", "sylvie" };
foreach (string nom in amis) {
Console.WriteLine(nom);
}
将显示:
3.4.4.2. 重复次数未知
C# 中有许多适用于这种情况的结构。
结构 tantque (while)
while(condition){
actions;
}
只要条件成立,我们就继续循环。该循环可能永远不会被执行。
注:
- 条件用方括号括起来。
- 每个操作以分号结尾。
- 只有当存在多个操作时才需要大括号。
- 大括号后不跟分号。
相应的算法结构是 tantque :
重复结构直到(do while)
语法如下:
do{
instructions;
}while(condition);
我们循环直到条件变为假。在此情况下,循环至少执行一次。
注释
- 条件用方括号括起来。
- 每个操作以分号结尾。
- 只有当存在多个操作时才需要大括号。
- 大括号后不跟分号。
对应的算法结构是 repeat ... until:
通用(for)结构
语法如下:
for(instructions_départ;condition;instructions_fin_boucle){
instructions;
}
只要条件为真(在每次循环迭代前进行评估),程序就会继续循环。首次进入循环前会执行起始语句,每次循环结束后会执行循环结束语句。
注释
- instructions_depart 和 instructions_fin_boucle 中的各项指令需用逗号分隔。
相应的算法结构如下:
示例
以下代码片段均计算前10个整数的和。
int i, somme, n=10;
for (i = 1, somme = 0; i <= n; i = i + 1)
somme = somme + i;
for (i = 1, somme = 0; i <= n; somme = somme + i, i = i + 1) ;
i = 1; somme = 0;
while (i <= n) { somme += i; i++; }
i = 1; somme = 0;
do somme += i++;
while (i <= n);
3.4.4.3. 循环管理指令
用于退出 loop、while 和 do ... while 循环。 | |
将 for、while、do ... while 循环推进到下一次迭代。while |
3.5. 异常处理
许多 C# 函数可能会引发异常,即错误。当一个函数可能引发异常时,程序员应进行异常处理,以期获得更具容错能力的程序:必须始终避免应用程序发生剧烈的“崩溃”。
异常的处理方法如下:
try{
code susceptible de générer une exception
} catch (Exception e){
traiter l'exception e
}
instruction suivante
如果函数未抛出异常,则执行下一条指令;否则,进入 catch 子句的主体,然后执行下一条指令。请注意以下几点:
- e 是一个 Exception 类型或其派生类型的对象。您可以通过使用 IndexOutOfRangeException、FormatException、SystemException 等具体类型来更精确地指定:异常类型多种多样。编写 catch (Exception e) 表示您希望处理所有类型的异常。如果 try 块中的代码可能引发多种类型的异常,您可能需要通过使用多个 catch 语句来更精确地处理异常:
try{
code susceptible de générer les exceptions
} catch ( IndexOutOfRangeException e1){
traiter l'exception e1
}
} catch ( FormatException e2){
traiter l'exception e2
}
instruction suivante
- 我们可以在 try/catch 语句中添加一个 finally 语句:
try{
code susceptible de générer une exception
} catch (Exception e){
traiter l'exception e
}
finally{
code exécuté après try ou catch
}
instruction suivante
无论是否发生异常,finally 代码块都会被执行。
- 在 catch 块中,您可能不希望使用可用的 Exception 类型。因此,不要写成 catch (Exception e){..},而应写成 catch(Exception){...} 或简写为 catch {...}。
- Exception 类有一个名为 Message 的属性,它详细描述了发生的错误。因此,如果我们要显示该信息,应编写如下代码:
catch (Exception ex){
Console.WriteLine("L'erreur suivante s'est produite : {0}",ex.Message);
...
}//catch
- Exception 类有一个 ToString 方法,该方法返回一个字符串,其中包含异常的类型和 Message 的值。因此,我们可以这样写:
catch (Exception ex){
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.ToString());
...
}//catch
我们还可以这样写:
编译器会将 ex.ToString() 的值赋给 {0} 参数。
以下示例展示了因使用不存在的数组元素而引发的异常:
using System;
namespace Chap1 {
class P08 {
static void Main(string[] args) {
// declaring & initializing an array
int[] tab = { 0, 1, 2, 3 };
int i;
// table display with for
for (i = 0; i < tab.Length; i++)
Console.WriteLine("tab[{0}]={1}", i, tab[i]);
// table display with a for each
foreach (int élmt in tab) {
Console.WriteLine(élmt);
}
// generating an exception
try {
tab[100] = 6;
} catch (Exception e) {
Console.Error.WriteLine("L'erreur suivante s'est produite : " + e);
return;
}//try-catch
finally {
Console.WriteLine("finally ...");
}
}
}
}
在上文中,第 18 行会引发异常,因为数组 tab 中不存在第 100 个元素。运行程序会得到以下结果:
- 第 9 行:发生异常 [System.IndexOutOfRangeException]
- 第 11 行:尽管第 21 行包含一个 return 语句用于退出方法,但代码中的 finally 子句(第 23-25 行)仍被执行。请注意,finally 子句总是会被执行。
以下是另一个示例,演示如何处理将字符串赋值给整型变量时(该字符串不表示整数)引发的异常:
using System;
namespace Chap1 {
class P08 {
static void Main(string[] args) {
// example 2
// We ask for the name
Console.Write("Nom : ");
// reading response
string nom = Console.ReadLine();
// age requested
int age = 0;
bool ageOK = false;
while (!ageOK) {
// question
Console.Write("âge : ");
// read-verify answer
try {
age = int.Parse(Console.ReadLine());
ageOK = age>=1;
} catch {
}//try-catch
if (!ageOK) {
Console.WriteLine("Age incorrect, recommencez...");
}
}//while
// final display
Console.WriteLine("Vous vous appelez {0} et vous avez {1} an(s)",nom,age);
}
}
}
- 第 15-27 行:用于输入人物年龄的循环
- 第 20 行:通过 int.Parse 将键盘输入的字符串转换为整数。如果无法进行转换,该方法会抛出异常。这就是为什么该操作被放在 try/catch 块中的原因。
- 第22-23行:若抛出异常,则进入catch块,该处不执行任何操作。因此,第14行设为false的布尔变量ageOK将保持false状态。
- 第 21 行:若执行到此行,说明字符串转整数操作已成功。但需检查所得整数是否大于等于 1。
- 第24-26行:如果年龄不正确,则会输出错误信息。
部分性能测试结果:
3.6. 应用示例 - V1
我们建议编写一个程序来计算纳税人的所得税。简化情况是纳税人仅需申报工资收入(2004年数据,对应2003年收入):
- 员工份额数按以下方式计算:nbParts=nbEnfants/2 +1(未婚),nbEnfants/2+2(已婚),其中nbEnfants代表其子女数。
- 若其子女数至少为三名,则额外增加半个份额
- 计算应税收入 R=0.72*S,其中 S 为其年薪
- 计算您的家庭系数 QF=R/nbParts
- 计算您的税额 I. 请参考下表:
4262 | 0 | 0 |
8382 | 0.0683 | 291.09 |
14753 | 0.1914 | 1322.92 |
23888 | 0.2826 | 2668.39 |
38868 | 0.3738 | 4846.98 |
47932 | 0.4262 | 6883.66 |
0 | 0.4809 | 9505.54 |
每行有 3 个字段。要计算税款 I,请查找 QF<=champ1 的第一行。例如,如果 QF=5000,则找到该行
税额 I 即等于 0.0683*R - 291.09*nbParts。如果 QF 使得关系 QF<=champ1 从未被检查,则使用最后一行中的系数。此处:
0 0.4809 9505.54
由此得出税款 I=0.4809*R - 9505.54*nbParts。
相应的 C# 程序如下:
using System;
namespace Chap1 {
class Impots {
static void Main(string[] args) {
// data tables required for tax calculation
decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
// marital status is restored
bool OK = false;
string reponse = null;
while (!OK) {
Console.Write("Etes-vous marié(e) (O/N) ? ");
reponse = Console.ReadLine().Trim().ToLower();
if (reponse != "o" && reponse != "n")
Console.Error.WriteLine("Réponse incorrecte. Recommencez");
else OK = true;
}//while
bool marie = reponse == "o";
// number of children
OK = false;
int nbEnfants = 0;
while (!OK) {
Console.Write("Nombre d'enfants : ");
try {
nbEnfants = int.Parse(Console.ReadLine());
OK = nbEnfants >= 0;
} catch {
}// try
if (!OK) {
Console.WriteLine("Réponse incorrecte. Recommencez");
}
}// while
// salary
OK = false;
int salaire = 0;
while (!OK) {
Console.Write("Salaire annuel : ");
try {
salaire = int.Parse(Console.ReadLine());
OK = salaire >= 0;
} catch {
}// try
if (!OK) {
Console.WriteLine("Réponse incorrecte. Recommencez");
}
}// while
// calculating the number of shares
decimal nbParts;
if (marie) nbParts = (decimal)nbEnfants / 2 + 2;
else nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3) nbParts += 0.5M;
// taxable income
decimal revenu = 0.72M * salaire;
// family quotient
decimal QF = revenu / nbParts;
// search for tax bracket corresponding to QF
int i;
int nbTranches = limites.Length;
limites[nbTranches - 1] = QF;
i = 0;
while (QF > limites[i]) i++;
// tax
int impots = (int)(coeffR[i] * revenu - coeffN[i] * nbParts);
// the result is displayed
Console.WriteLine("Impôt à payer : {0} euros", impots);
}
}
}
- 第 7-9 行:数值后缀为 M(Money),表示其类型为 decimal。
- 第16行:
- Console.ReadLine() 使字符串 C1 具有类型
- C1.Trim() 去除字符串首尾的空格C1 - 生成字符串 C2
- C2.ToLower() 将字符串 C2 转换为小写,生成字符串 C3。
- 第 21 行:布尔变量 marie 接收值 true 或 false,取决于关系式 answer=="o"
- 第 29 行:键盘输入的字符串被转换为整数。如果转换失败,将抛出异常。
- 第 30 行:布尔变量 OK 根据条件 nbEnfants>=0 取值为 true 或 false
- 第 55-56 行:不能直接写成 nbEnfants/2。如果 nbEnfants 等于 3,结果将是 3/2,这种整数除法会得到 1 而不是 1.5。因此,我们写成 (decimal)nbEnfants,使除法的一个操作数为实数,从而进行实数除法。
以下是一些示例:
Etes-vous marié(e) (O/N) ? oui
Réponse incorrecte. Recommencez
Etes-vous marié(e) (O/N) ? o
Nombre d'enfants : trois
Réponse incorrecte. Recommencez
Nombre d'enfants : 3
Salaire annuel : 60000 euros
Réponse incorrecte. Recommencez
Salaire annuel : 60000
Impôt à payer : 2959 euros
3.7. 主程序参数
主函数 Main 可以接受一个字符串数组作为参数:String[](或 string[])。该数组包含用于启动应用程序的命令行参数。例如,如果我们使用以下命令(DOS)运行程序 P:
P arg0 arg1 … argn
且 Main 函数声明如下:
args[0]="arg0", args[1]="arg1" ... 以下是一个示例:
using System;
namespace Chap1 {
class P10 {
static void Main(string[] args) {
// list parameters received
Console.WriteLine("Il y a " + args.Length + " arguments");
for (int i = 0; i < args.Length; i++) {
Console.Out.WriteLine("arguments[" + i + "]=" + args[i]);
}
}
}
}
要向执行的代码传递参数,请按以下步骤操作:
![]() |
- 在 [1] 中:右键单击项目 / 属性
- 在 [2] 中:[调试] 选项卡
- 在 [3] 中:输入参数
执行后得到以下结果:
请注意,该
在 Main 方法不期望任何参数时是有效的。
3.8. 枚举
枚举是一种其值域为一组整数常量的数据类型。让我们考虑一个需要管理考试成绩的程序。考试成绩共有五种:Fair、AssezBien、Fine、TrèsBien、Excellent。
我们可以为这五个常量定义一个枚举:
enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };
在内部,这五个常量由连续的整数编码表示,第一个常量对应 0,第二个常量对应 1,依此类推……可以声明一个变量,使其取值范围为该枚举中的值:
// une variable qui prend ses valeurs dans l'énumération Mentions
Mentions maMention = Mentions.Passable;
变量可以与枚举中的各种可能值进行比较:
if (maMention == Mentions.Passable) {
Console.WriteLine("Peut mieux faire");
}
您可以获取枚举的所有值:
// list of statements in string form
foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
Console.WriteLine(m);
}
正如简单类型 int 等同于 System.Int32 一样,简单类型 enum 也等同于 System.Enum。该结构拥有一个静态方法 GetValues,它允许您获取作为参数传递的枚举类型的所有值。该参数必须是 Type 类型的对象,Type 是一个关于数据类型的信息类。 变量 v 的类型可通过 v.GetType() 获取。类型 T 的类型可通过 typeof(T) 获取。因此,此处的 maMention.GetType() 返回枚举类型 Mentions 的 Type 对象,而 Enum.GetValues(maMention.GetType()) 则返回枚举类型 Mentions 的值列表。
如果现在我们编写
//list of mentions in integer form
foreach (int m in Enum.GetValues(typeof(Mentions))) {
Console.WriteLine(m);
}
第 2 行,循环变量为整数类型。随后我们获取了整数形式的枚举值列表。通过 typeof(Mentions) 获取了与数据类型 Mentions 对应的 System.Type 对象。我们也可以写成 maMention.GetType()。
以下程序演示了上述内容:
using System;
namespace Chap1 {
class P11 {
enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };
static void Main(string[] args) {
// a variable that takes its values from the Mentions enumeration
Mentions maMention = Mentions.Passable;
// variable value display
Console.WriteLine("mention=" + maMention);
// test with enumeration value
if (maMention == Mentions.Passable) {
Console.WriteLine("Peut mieux faire");
}
// list of statements in string form
foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
Console.WriteLine(m);
}
//list of mentions in integer form
foreach (int m in Enum.GetValues(typeof(Mentions))) {
Console.WriteLine(m);
}
}
}
}
结果如下:
3.9. 向函数传递参数
这里我们关注参数如何传递给函数。请看以下这个静态函数:
private static void ChangeInt(int a) {
a = 30;
Console.WriteLine("Paramètre formel a=" + a);
}
在函数定义的第1行中,a被称为形式参数。它的存在仅是为了定义changeInt函数。它完全也可以被命名为b。现在让我们来看一下该函数的一个用法:
public static void Main() {
int age = 20;
ChangeInt(age);
Console.WriteLine("Paramètre effectif age=" + age);
}
在此第 3 行语句 ChangeInt(age) 中,age 是实际参数,它将把自己的值传递给形式参数 a。我们关注的是形式参数如何获取实际参数的值。
3.9.1. 按值传递
以下示例说明,默认情况下,函数参数采用值传递,即实际参数的值会被复制到对应的形式参数中。这两个参数是相互独立的实体。如果函数修改了形式参数,实际参数的值将保持不变。
using System;
namespace Chap1 {
class P12 {
public static void Main() {
int age = 20;
ChangeInt(age);
Console.WriteLine("Paramètre effectif age=" + age);
}
private static void ChangeInt(int a) {
a = 30;
Console.WriteLine("Paramètre formel a=" + a);
}
}
}
结果如下:
实际参数 age 的值 20 已被复制到形式参数 a 中(第 10 行)。随后该值被修改(第 11 行)。实际参数保持不变。此模式适用于函数输入参数。
3.9.2. 按引用传递
在引用传递中,有效参数与形式参数是同一个实体。如果函数修改了形式参数,有效参数也会随之改变。在 C# 中,两者都必须在前面加上关键字 ref:
以下是一个示例:
using System;
namespace Chap1 {
class P12 {
public static void Main() {
// example 2
int age2 = 20;
ChangeInt2(ref age2);
Console.WriteLine("Paramètre effectif age2=" + age2);
}
private static void ChangeInt2(ref int a2) {
a2 = 30;
Console.WriteLine("Paramètre formel a2=" + a2);
}
}
}
以及性能结果:
实际参数已跟随形式参数的修改。此模式适用于函数的输出参数。
3.9.3. 使用 out 关键字进行引用传递
考虑前面的示例,其中变量 age2 在调用 changeInt 之前不会被初始化:
using System;
namespace Chap1 {
class P12 {
public static void Main() {
// example 2
int age2;
ChangeInt2(ref age2);
Console.WriteLine("Paramètre effectif age2=" + age2);
}
private static void ChangeInt2(ref int a2) {
a2 = 30;
Console.WriteLine("Paramètre formel a2=" + a2);
}
}
}
编译此程序时,会出现以下错误:
我们可以通过为 age2 赋初始值来解决这个问题。你也可以将关键字 ref 替换为关键字 out。这样就表明该参数仅为输出参数,因此无需初始值:
using System;
namespace Chap1 {
class P12 {
public static void Main() {
// example 3
int age3;
ChangeInt3(out age3);
Console.WriteLine("Paramètre effectif age3=" + age3);
}
private static void ChangeInt3(out int a3) {
a3 = 30;
Console.WriteLine("Paramètre formel a3=" + a3);
}
}
}
结果如下:





