Skip to content

3. C# 语言基础

3.1. Introduction

我们首先将 C# 视为一种经典的编程语言。关于类的讨论将在后续进行。在程序中包含两部分内容:

  • 数据
  • 操作数据的指令

通常我们力求将数据与指令分离:

3.2. C# 中的数据

C# 使用以下数据类型:

  1. 整数
  1. 实数
  2. 十进制数
  3. 字符和字符串
  4. 布尔值
  5. 对象

3.2.1. 预定义的数据类型

C# 类型
类型 .NET
表示的数据
后缀
字面值
编码
值域
char
字符 (S)
字符
 
2字节
Unicode字符 (UTF-16)
string
字符串 (C)
字符串
  
Unicode字符序列的引用
int
Int32 (S)
整数
 
4字节
[-231, 231-1][–2147483648, 2147483647]
uint
UInt32 (S)
..
U
4字节
[0, 232-1][0, 4294967295]
long
Int64 (S)
..
L
8字节
[-263, 263 -1][–9223372036854775808, 9223372036854775807]
ulong
UInt64 (S)
..
UL
8 字节
[0, 264 -1][0, 18446744073709551615]
sbyte
 
..
 
1字节
[-27 , 27 -1] [-128,+127]
byte
字节 (S)
..
 
1 字节
[0 , 28 -1][0,255]
short
16位整数 (S)
..
 
2 字节
[-215, 215-1][-32768, 32767]
ushort
UInt16 (S)
..
 
2字节
[0, 216-1] [0,65535]
float
单数 (S)
实数
F
4 字节
[1.5 10-45, 3.4 10+38] 绝对值
double
双精度 (S)
..
D
8字节
[-1.7 10+308, 1.7 10+308] 绝对值
decimal
十进制 (S)
十进制数
M
16字节
[1.0 10-28,7.9 10+28],绝对值,有效位数为28位
bool
布尔型 (S)
..
 
1 字节
true, false
object
对象 (C)
对象引用
  
对象引用

上表中,我们列出了 C# 类型及其等效的 .NET 类型,并附有注释:若该类型为结构体则标注 (S),若为类则标注 (C)。 我们发现,对于 32 位整数,有两种可能的类型:intInt32。 类型 int 是一个 C# 类型。Int32 是属于命名空间 System 的结构。因此其全名为 System.Int32。 类型 int 是 C# 别名,指代结构 .NET System.Int32。 同样,C# 类型 string 是类型 .NET System.String 的别名。System.String 是一个,而不是结构。这两个概念虽然相似,但存在以下根本区别:

  • Structure 类型的变量通过其进行操作
  • 而类型为 Classe 的变量则通过其地址(面向对象语言中的引用)进行操作。

无论是 structure 还是 classe,都是具有属性和方法的复杂类型。因此,我们可以这样写:

string nomDuType=3.GetType().FullName;

上文中的数字 3 默认属于 C# 类型 int,因此属于 .NET System.Int32 类型。 该结构有一个方法 GetType(),该方法返回一个封装了 System.Int32 数据类型特征的对象。其中,属性 FullName 返回该类型的完整名称。 由此可见,字面量 3 是一个比乍看之下更为复杂的对象。

以下是一个说明这些要点的程序:


using System;

namespace Chap1 {
    class P00 {
        static void Main(string[] args) {
            // 示例 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

以下是执行结果:

Type de ent[2] : [System.Int32,4]
Type de fl[10,5]: [System.Single,4]
Type de d[-4,6] : [System.Double,8]
Type de s[essai] : [System.String]
Type de ui[5] : [System.UInt32,4]
Type de l[1000] : [System.Int64,8]
Type de ul[1001] : [System.UInt64,8]
Type de b[5] : [System.Byte,1]
Type de sh[-4] : [System.Int16,2]
Type de ush[10] : [System.UInt16,2]
Type de dec[10,67] : [System.Decimal,16]
Type de b[True] : [System.Boolean,1]

显示结果为类型 .NET,而非 C# 别名。

3.2.2. 字面量表示法

entier int (32 bits)
145, -7, 0xFF (十六进制)
entier long (64 bits) - suffixe L
100000L
réel double
134.789, -45E-18 (-45 × 10⁻¹⁸)
réel float (suffixe F)
134.789F, -45E-18F (-45×10⁻¹⁸)
réel decimal (suffixe M)
100000M
caractère char
'A', 'b'
chaîne de caractères string
"今天" "c:\\chap1\\paragraph3" @"c:\chap1\paragraph3"
booléen bool
true, false
date
new DateTime(1954,10,13) (年, 月, 日) 表示 1954年10月13日

请注意以下两个字符串:"c:\\chap1\\paragraph3" 和 @"c:\chap1\paragraph3"。在字符串中,字符 \ 会被解释。 因此 "\n" 代表换行符,而非 \ 和 n 这两个字符的组合。若需表示该字符组合,应写为 "\\n",此时 \\ 序列会被解释为单个 \ 字符。也可写成 @"\n" 以获得相同效果。语法 @"文本" 要求文本被原样保留。 这有时被称为原样字符串。

3.2.3. 数据声明

3.2.3.1. 声明的作用

程序处理的数据由名称和类型来定义。这些数据存储在内存中。在程序编译时,编译器会根据程序员的声明,为每项数据分配一个由地址和大小定义的内存位置。

此外,这些声明还能帮助编译器检测编程错误。例如,运算

x=x*2;

若 x 是一个字符串,则会被判定为错误。

3.2.3.2. 常量的声明

常量的声明语法如下:

    const type nom=valeur;          //定义常量 名称=值

例如:

const float myPI=3.141592F;    

为什么要声明常量?

  1. 如果给常量起一个有意义的名称,程序将更易于阅读:
    const  float taux_tva=0.186F;
  1. 如果“常量”需要更改,修改程序也会更加容易。因此,在前面的例子中,如果增值税税率变为33%,唯一需要做的修改就是修改定义其值的语句:
    const float taux_tva=0.33F;

如果程序中直接使用了0.186这个数值,那么就需要修改大量语句。

3.2.3.3. 变量声明

变量通过名称进行标识,并关联一种数据类型。C# 区分大小写。因此,变量 FINfin 是不同的。

变量可以在声明时进行初始化。声明一个或多个变量的语法如下:

 Identificateur_de_type variable1[=valeur1],variable2=[valeur2],...;

其中 Identificateur_de_type 是一个预定义类型或由程序员定义的类型。可选地,变量可以在声明时同时进行初始化。

也可以不指定变量的确切类型,使用关键字 var 代替 Identificateur_de_type

 var variable1=valeur1,variable2=valeur2,...;

关键字 var 并不意味着变量没有确切的类型。变量 variablei 的类型取决于赋予它的数据 valeuri。此处的初始化是强制性的,以便编译器能够推断出变量的类型。

以下是一个示例:


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 行:(数据).GetType()。Name 是 (数据) 的短名称,(数据).GetType().FullName 是 (数据) 的全名
  • 第 8 行:隐式类型的数据。因为 3 的类型是 int,所以 j 的类型将是 int
  • 第10行:隐式类型化的数据。由于 DateTime.Now 的类型为 DateTime,因此 aujourdhui 的类型为 DateTime

运行时,得到以下结果:

1
2
3
Type de int i=2 : Int32,System.Int32
Type de var j=3 : Int32,System.Int32
Type de var aujourdhui : DateTime,System.DateTime

通过关键字 var 隐式定义类型的变量,其类型随后无法更改。因此,在代码第 10 行之后,无法编写以下代码行:


            var aujourdhui = "aujourd'hui";

我们稍后将看到,可以在表达式中“动态”声明一个类型。这种类型被称为匿名类型,即用户未为其命名的类型。由编译器为该新类型命名。如果需要将匿名类型的数据赋值给变量,声明该变量的唯一方法是使用关键字 var

3.2.4. 数字与字符串之间的转换

nombre -> chaîne
nombre.ToString()
chaine -> int
int.Parse(字符串) 或 System.Int32.Parse
chaîne -> long
long.Parse(字符串) 或 System.Int64.Parse
chaîne -> double
double.Parse(字符串)或 System.Double.Parse(字符串)
chaîne -> float
float.Parse(字符串)或 System.Float.Parse(字符串)

如果字符串不代表一个有效的数字,则字符串到数字的转换可能会失败。 此时会产生一个称为异常的致命错误。可以通过以下 try/catch 子句来处理此错误:


try{
    appel de la fonction susceptible de générer l'exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

如果该函数未引发异常,则执行下一条指令;否则,进入子句 catch 的主体,然后执行下一条指令。我们稍后将详细讨论异常处理。 以下是一个展示数字与字符串之间转换技巧的程序。在此示例中,函数 affiche 会将参数的值输出到屏幕上。因此,affiche(S) 会将 S 的值输出到屏幕上,其中 S 的类型为 string


using System;

namespace Chap1 {
    class P01 {
        static void Main(string[] args) {

            // 数据
            const int i = 10;
            const long l = 100000;
            const float f = 45.78F;
            double d = -14.98;

            // 数字 --> 字符串
            affiche(i.ToString());
            affiche(l.ToString());
            affiche(f.ToString());
            affiche(d.ToString());

            //布尔值 --> 字符串
            const bool b = false;
            affiche(b.ToString());

            // 字符串 --> 整数
            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);
            }

            // 字符串 --> 长整型
            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);
            }

            // 字符串 --> 双精度
            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);
            }

            // 字符串 --> 浮点数
            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);
            }

        }// 结束 main

        public static void affiche(string S) {
            Console.Out.WriteLine("S={0}",S);
        }
    }// 类结束
}

第 30-32 行,处理可能发生的异常。e.Message 是与异常 e 相关的错误消息。

所得结果如下:

S=10
S=100000
S=45,78
S=-14,98
S=False
S=10
S=Erreur : Input string was not in a correct format.
S=100
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.

需要注意的是,以字符串形式表示的实数必须使用逗号,而非小数点。因此应写为

double d1=10.7; 

double d2=int.Parse("10,7");

3.2.5. 数据数组

C# 数组是一个对象,用于将同类型的数据集合在同一个标识符下。其声明如下:

Type[] tableau[]=new Type[n]

n 是数组可容纳的数据数量。语法 Tableau[i] 表示编号为 i 的数据,其中 i 属于区间 [0,n-1]。 任何对数据 Tableau[i] 的引用,若 i 不属于区间 [0,n-1],都将引发异常。数组可以在声明时同时初始化:

    int[] entiers=new int[] {0,10,20,30};

或者更简洁地:

    int[] entiers={0,10,20,30};

数组有一个 Length 属性,表示数组中的元素个数。

二维数组可以如下声明:

Type[,] 数组 = new Type[n,m];

其中 n 表示行数,m 表示列数。语法 Tableau[i,j] 表示数组第 i 行中的第 j 个元素。二维数组也可以在声明时同时进行初始化:

    double[,] réels=new double[,] { {0.5, 1.7}, {8.4, -6}};

或者更简洁地写为:

    double[,] réels={ {0.5, 1.7}, {8.4, -6}};

每个维度的元素数量可通过方法 GetLength(i) 获取,其中 i=0 表示对应于第 1 个索引的维度,i=1 表示对应于第 2 个索引的维度,……

维度的总数可通过Rank属性获取,元素的总数可通过Length属性获取。

多维数组的声明如下:

Type[][] array = new Type[n][];

上述声明创建了一个包含 n 行元素的数组。每个 tableau[i] 元素都是指向一维数组的引用。在上述声明中,这些 tableau[i] 引用并未被初始化,其值为 null 引用。

下面的示例演示了如何创建一个数组数组:


            // 数组数组
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//for
            // 初始化
            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 行:一个包含 3 个 string[][] 类型元素的 noms 数组。每个元素都是一个数组指针(即对象引用),其元素类型为 string[]
  • 第3-5行:数组noms的3个元素被初始化。现在,每个元素都“指向”一个由类型为string[]的元素组成的数组。 noms[i][j] 是由 noms[i] 引用的字符串数组 [] 中的 j 元素。
  • 第 9 行:在双重循环中初始化元素 noms[i][j]。 此处 noms[i] 是一个包含 i+1 个元素的数组。由于 noms[i] 是一个数组,因此 noms[i].Length 即为其元素个数。

以下是一个示例,综合了我们刚刚介绍的三种数组类型:


using System;

namespace Chap1 {
    // 数组

    using System;

    // 测试类
    public class P02 {
        public static void Main() {
            // 一个已初始化的一维数组
            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

            // 一个已初始化的二维数组
            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

            // 一个数组数组
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//用于
            // 初始化
            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
            // 显示
            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]);
                }//用于 j
            }//用于 i
        }//主
    }//类
}//命名空间

运行后,我们得到以下结果:

entiers[0]=0
entiers[1]=10
entiers[2]=20
entiers[3]=30
réels[0,0]=0,5
réels[0,1]=1,7
réels[1,0]=8,4
réels[1,1]=-6
noms[0][0]=nom00
noms[1][0]=nom10
noms[1][1]=nom11
noms[2][0]=nom20
noms[2][1]=nom21
noms[2][2]=nom22

3.3. C# 的基本语句

可分为

1 由计算机执行的基本指令。

2 控制程序运行的指令。

当我们考察微型计算机及其外围设备的结构时,基本指令便显而易见。

  1. 读取来自键盘的信息

  2. 信息处理

  3. 将信息写入屏幕

3.3.1. 屏幕输出

存在多种屏幕写入指令:

Console.Out.WriteLine(expression)
Console.WriteLine(expression)
Console.Error.WriteLine (expression)

其中 expression 代表任何可转换为字符串并在屏幕上显示的数据类型。所有 C# 或 .NET 对象都提供 ToString() 方法,用于执行此转换。

System.Console 类提供了屏幕写入操作(Write, WriteLine)。Console 类具有两个属性 Out 和 Error,它们是 TextWriter 类型的写入流

  • Console.WriteLine()Console.Out.WriteLine() 功能等同,并向通常与屏幕关联的 Out 流写入数据。
  • Console.Error.WriteLine() 会向 Error 流写入数据,该流通常也与屏幕相关联。

OutError 数据流可在程序运行时重定向到文本文件,我们稍后将对此进行说明。

3.3.2. 读取键盘输入的数据

来自键盘的数据流由类型为 TextReader 的对象 Console.In 表示。此类对象可通过 ReadLine 方法读取一行文本:

    string ligne=Console.In.ReadLine();

Console 类提供了一个默认关联到数据流 In 的方法 ReadLine。因此可以编写如下代码:

    string ligne=Console.ReadLine();

键盘输入的行会被存储在变量 ligne 中,随后可由程序进行处理。In 流可以重定向到文件,就像 OutError 流一样。

3.3.3. 输入输出示例

以下是一个简短的程序,用于演示键盘/屏幕的输入输出操作:


using System;

namespace Chap1 {
    // 测试类
    public class P03 {
        public static void Main() {

            // 写入 Out 流
            object obj = new object();
            Console.Out.WriteLine(obj);

            // 向 Error 流写入
            int i = 10;
            Console.Error.WriteLine("i=" + i);

            // 读取键盘输入的一行
            Console.Write("Tapez une ligne : ");
            string ligne = Console.ReadLine();
            Console.WriteLine("ligne={0}", ligne);
        }//main 结束
    }//类结束
}
  • 第 9 行:obj 是一个对象引用
  • 第 10 行:obj 被写入屏幕。默认情况下,调用的是 obj.ToString() 方法。
  • 第 14 行:也可以这样写:

            Console.Error.WriteLine("i={0}",i);

第一个参数“i={0}”是显示格式,其余参数则是待显示的表达式。{n} 元素是“位置”参数。在执行时,参数 {n} 将被第 n 个表达式的值替换。

执行结果如下:

1
2
3
4
System.Object
i=10
Tapez une ligne : je suis là
ligne=je suis là
  • 第 1 行:由代码第 10 行生成的显示内容。方法 obj.ToString() 显示了变量 obj 的类型名称:System.Object。 类型 object 是类型 .NET System.Object 的 C# 别名。

3.3.4. I/O 重定向

在 DOS 和 UNIX 下,有三个标准设备,分别称为:

  1. 标准输入设备——默认指键盘,编号为 0
  2. 标准输出设备——默认指向屏幕,编号为 1
  3. 标准错误设备——默认指向屏幕,编号为 2

在 C# 中,写入流 Console.Out 写入设备 1,写入流 Console.Error 写入设备 2,而读取流 Console.In 读取来自设备 0 的数据。

在 DOS 或 Unix 环境下运行程序时,可以指定该程序所使用的设备 0、1 和 2。请看以下命令行:

pg arg1 arg2 .. argn

在程序 pg 的参数 argi 之后,可以将标准 I/O 设备重定向到文件:

0<in.txt
标准输入流 0 被重定向到文件 in.txt。因此,在程序中,流 Console.In 将从文件 in.txt 中获取数据。
1>out.txt
将输出流 1 重定向至文件 out.txt。这导致在程序中,数据流 Console.Out 会将数据写入文件 out.txt
1>>out.txt
同上,但写入的数据将追加到文件 out.txt 的现有内容中。
2>error.txt
将输出端口2重定向至文件error.txt。这意味着在程序中,数据Console.Error将把数据写入文件error.txt
2>>error.txt
同上,但写入的数据将追加到文件 error.txt 的现有内容中。
1>out.txt 2>error.txt
设备 1 和 2 均被重定向到文件

需要注意的是,为了将程序 pg 的 I/O 流重定向到文件,无需修改程序 pg。操作系统会确定设备 0、1 和 2 的性质。考虑以下程序:


using System;

namespace Chap1 {

    // 重定向
    public class P04 {
        public static void Main(string[] args) {
            // 读取 In 数据流
            string data = Console.In.ReadLine();
            // 写入 Out 流
            Console.Out.WriteLine("écriture dans flux Out : " + data);
            // 写入错误流
            Console.Error.WriteLine("écriture dans flux Error : " + data);
        }//主类
    }//类
}

生成该源代码的可执行文件:

  • 生成 [1]:通过右键单击项目 / 构建
  • 在 [2] 中:在 DOS 窗口中,可执行文件 04.exe 已创建在项目的 bin/Release 文件夹中。

在 DOS 窗口中输入以下命令 [2]:

1
2
3
4
5
6
7
8
...\04\bin\Release>echo test >in.txt
...\04\bin\Release>more in.txt
test
...\04\bin\Release>04 0<in.txt 1>out.txt 2>err.txt
...\04\bin\Release>more out.txt
écriture dans flux Out : test
...\04\bin\Release>more err.txt
écriture dans flux Error : test
  • 第 1 行:将字符串 test 写入文件 in.txt
  • 第 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进行类似验证

可以清楚地看到,OutIn 数据流并未写入相同的设备,因为我们能够将它们分别重定向。

3.3.5. 将表达式的值赋给变量

这里我们关注操作 variable=expression;

表达式可以是:算术型、关系型、布尔型、字符型

3.3.5.1. 赋值运算的解释

运算 variable=expression;

本身是一个表达式,其求值过程如下:

  • 先计算赋值运算的右侧:结果为值 V。
  • 将值 V 赋给变量
  • 值 V 也是赋值操作的值,此时该操作被视为一个表达式。

因此,运算

    V1=V2=expression

是合法的。由于运算符优先级,最右侧的 = 运算符将被优先求值。因此,我们得到

    V1=(V2=expression)

表达式 V2=expression 被求值,其值为 V。该表达式的求值导致 V 被赋值为 V2。随后,下一个 = 运算符以以下形式被求值:

    V1=V

该表达式的值仍是 V。其求值导致 V 被赋值给 V1。

因此,运算 V1=V2=表达式

是一个表达式,其求值

  • expression 的值赋给变量 V1 和 V2
  • 返回值为 expression

我们可以将其推广为如下形式的表达式:

V1=V2=....=Vn=expression

3.3.5.2. 算术表达式

算术表达式的运算符如下:

  • 加法

  • 减法

* 乘法

/ 除法:如果至少有一个操作数是实数,则结果为精确商。如果两个操作数都是整数,则结果为整数商。因此 5/2 -> 2,而 5.0/2 -> 2.5。

% 除法:无论操作数的性质如何,结果均为余数,商为整数。因此这是取模运算。

存在多种数学函数。以下列举其中几种:

double Sqrt(double x)
平方根
double Cos(double x)
余弦
double Sin(double x)
正弦
double Tan(double x)
正切
double Pow(double x,double y)
x的y次方 (x>0)
double Exp(double x)
指数函数
double Log(double x)
自然对数
double Abs(double x)
绝对值

等等...

所有这些函数都在一个名为 Math 的 C# 类中定义。使用时,必须在函数名前加上其所属类的名称。因此,我们应这样写:

double x, y=4;
x=Math.Sqrt(y);

Math 的完整定义如下:

Image

Image

Image

Image

Image

3.3.5.3. 算术表达式求值中的运算优先级

在计算算术表达式时,运算符的优先级如下(从最高优先级到最低优先级):

[fonctions], [ ( )],[ *, /, %], [+, -]

同一块中的运算符具有相同的优先级。

3.3.5.4. 关系表达式

运算符如下:

       <, <=, ==, !=, >, >=

运算符优先级

>, >=, <, <=
==, !=

关系表达式的结果为布尔值 false(若表达式为假),否则为 true

      bool fin;
      int x=...;
      fin=x>4;

两个字符的比较

假设有两个字符 C1 和 C2。可以使用以下运算符进行比较

    <, <=, ==, !=, >, >=

此时,比较的是它们的 Unicode 码(即数字)。根据 Unicode 排序规则,它们之间存在以下关系:

espace < .. < '0' < '1' < .. < '9' < .. < 'A' < 'B' < .. < 'Z' < .. < 'a' < 'b' < .. <'z'

两个字符串的比较

字符串按字符逐个比较。若两个字符之间首次出现不等关系,则整个字符串也呈现相同方向的不等关系。

示例

假设要比较字符串“Chat”和“Chien”

最后一次不相等表明“Chat” < “Chien”。

假设要比较字符串“Chat”和“Chaton”。在“Chat”字符串耗尽之前,两者始终相等。在此情况下,耗尽的字符串被判定为“较小”的一方。因此,我们得到如下关系

    "Chat" < "Chaton".

两个字符串的比较函数

可以使用关系运算符 == 和 != 来判断两个字符串是否相等,或者使用类 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行,使用运算符==和!=来检查两个字符串是否相等。

执行结果:

i=-1, egal=False
chien==chaine1:False,chien!=chaine2:False

3.3.5.5. 布尔表达式

可用的运算符包括 AND (&&) OR(||) NOT (!)。布尔表达式的结果是一个布尔值。

运算符优先级

  1. !
  2. &&
  3. ||

            double x = 3.5;
            bool valide = x > 2 && x < 4;

关系运算符的优先级高于 && 和 || 运算符。

3.3.5.6. 位运算

运算符

ij 为两个整数。

i<<n
将 i 向左移 n 位。移入的位均为零。
i>>n
将 i 向右移位 n 位。如果 i 是带符号整数(signed char、int、long),则保留符号位。
i & j
对 i 和 j 进行位与运算。
i | j
执行 i 与 j 的按位逻辑或运算。
~i
将 i 补为 1
i^j
对 i 和 j 执行 OU EXCLUSIF 运算

假设以下代码:


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)。

执行结果如下:

i=0x0064, j=0xfff3, k=0xf123
i<<4=0x0640, i>>4=0x0006,k>>4=0x0f12,i&j=0x0060,i|j=0xfff7,~i=0xff9b,j<<2=0xffcc,j>>2=0xfffc

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=variable+1variable+=1

表示法 variable-- 表示 variable=variable-1variable-=1

3.3.5.9. 三元运算符?

表达式

    expr_cond ? expr1:expr2

按以下方式计算:

1 表达式 expr_cond 被求值。这是一个条件表达式,其值为 vraifaux

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. 运算符的一般优先级

() []  fonction                  
gd
! ~ ++ --                        
dg
new (type) opérateurs cast       
dg
*  /  %                          
gd
+  -                             
gd
<<  >>                           
gd
< <=  > >= instanceof            
gd
==    !=                         
gd
&                                
gd
^                                
gd
|                                
gd
&&                               
gd
||                               
gd
?   :                            
dg
= += -= etc. .                   
dg

gd 表示在优先级相同时,遵循从左到右的优先级规则。这意味着当表达式中存在优先级相同的运算符时,表达式中最左侧的运算符将首先被计算。dg 表示从右到左的优先级。

3.3.5.11. 类型转换

在表达式中,可以暂时改变一个值的编码。这被称为改变数据的类型,英文称为类型转换type casting)。在表达式中改变值类型的语法如下:

    (type) valeur

此时该值将采用指定的类型。这会导致该值的编码发生改变。


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。3/4 的除法是整数除法,因为两个操作数均为 int 类型。
  • 第8行,(float)i是i的值转换为float后的结果。现在,我们进行的是类型为float的实数与类型为int的整数之间的除法。 随后将进行实数之间的除法运算。j 的值也将转换为 float 类型,然后对这两个实数进行除法运算。此时 f2 的值为 0.75。

以下是执行结果:

f1=0, f2=0,75

在运算 (float)i 中:

  • i 是 2 字节精确编码的值
  • (float) i 是该值的 4 字节浮点近似编码

因此,i的值会进行类型转换。这种转换仅在计算过程中发生,变量i始终保持其int的类型。

3.4. 程序执行流程的控制语句

3.4.1. 停止

在类 Environment 中定义的方法 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 Shell 命令解释器时,变量 $? 会获取 status 的值。

    Environment.Exit(0);

将以状态值为 0 的方式终止程序的执行。

3.4.2. 简单选择结构

 syntaxe :  if (condition) {actions_condition_vraie;} else {actions_condition_fausse;}

  • 条件用圆括号括起。
  • 每个操作以分号结尾。
  • 大括号内不以分号结尾。
  • 只有当存在多个操作时才需要大括号。
  • 子句 else 可以省略。
  • 不存在 then 子句。

该结构的算法等价形式是“如果……那么……否则”结构:

示例


    if (x>0)  { nx=nx+1;sx=sx+x;} else dx=dx-x;

可以选择结构进行嵌套:

if(condition1)
if (condition2)
        {......}
      else         //条件2
         {......}
else         //条件1
     {.......}

有时会出现以下问题:


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");    

在此,我们希望将 else 映射到 if(n2>1),而非将 else 映射到 if(n2>6)。 鉴于前文所述,我们必须在 if(n2>1) {...} else ... 中添加大括号

3.4.3. 案例结构

语法如下:

switch(expression) {
    case v1:     
            actions1;
            break;
    case v2:     
            actions2;
            break;
         . .. .. .. .. ..
    default:     
            actions_sinon;
            break;
}

  • switch 的控制表达式值可以是整数、字符或字符串
  • 控制表达式被括号包围。
  • 子句 default 可以省略。
  • 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# 中

int choix = 2; 
bool erreur = false;
switch (choix) {
   case 0: return;
   case 1: M1(); break;
   case 2: M2(); break;
   default: erreur = true; break;
}
}// Main结束

static void M1() {
    Console.WriteLine("M1");
}

static void M2() {
    Console.WriteLine("M2");
}
}

3.4.4. 重复结构

3.4.4.1. 已知重复次数

for 结构

语法如下:


    for (i=id;i<=if;i=i+ip){ 
       actions; 
      } 

  • for 的 3 个参数位于圆括号内,并用分号分隔。
  • for中的每个操作都以分号结尾。
  • 只有当存在多个操作时才需要大括号。
  • 大括号后不跟分号。

其算法等价形式为结构 pour

pour i variant de id à if avec un pas de ip
    actions
finpour

这可以转换为结构 tantque

    i  id
    tantque i<=if
        actions
        i i+ip
    fintantque

foreach 结构

语法如下:


foreach (Type variable in collection)
    instructions; 
}

注释

  • collection 是一个可枚举对象集合。我们已知的可枚举对象集合是数组
  • Type 是集合中对象的类型。对于数组而言,这即为数组元素的类型
  • variable 是循环的局部变量,它将依次取集合中的所有值作为其值。

因此,以下代码:


            string[] amis = { "paul", "hélène", "jacques", "sylvie" };
            foreach (string nom in amis) {
                Console.WriteLine(nom);
         }

将显示:

paul
hélène
jacques
sylvie

3.4.4.2. 重复次数未知

C# 中有许多适用于此情况的结构。

while 循环


    while(condition){
          actions;
        } 

只要条件成立,循环就会继续。该循环可能永远不会被执行。

  • 条件用圆括号括起来。
  • 每个操作以分号结尾。
  • 只有当存在多个操作时才需要大括号。
  • 大括号后不加分号。

相应的算法结构是“直到”结构:

tantque condition
    actions
fintantque

循环结构(do while)

语法如下:


    do{
       instructions;
    }while(condition); 

循环执行直到条件变为假。在此情况下,循环至少执行一次。

  • 条件用圆括号括起来。
  • 每个操作以分号结尾。
  • 只有当存在多个操作时才需要大括号。
  • 大括号后不跟分号。

相应的算法结构是“重复……直到”结构:

répéter
    actions
jusqu'à condition

for 循环结构

语法如下:


for(instructions_départ;condition;instructions_fin_boucle){
    instructions; 
}

只要条件为真(在每次循环迭代前进行评估),循环就会持续进行。Instructions_départ 在首次进入循环前执行。Instructions_fin_boucle 在每次循环迭代后执行。

  • instructions_depart instructions_fin_boucle 中的各条指令以逗号分隔。

相应的算法结构如下:

instructions_départ
tantque condition
    actions
    instructions_fin_boucle
fintantque

示例

以下代码片段均计算前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. 循环管理说明

break
退出 for、while、do ... while 循环。
continue
进入 for、while、do ... 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

也可以写成:

catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}",ex);
    ...
}//catch

编译器将向参数 {0} 赋值 ex.ToString()

以下示例展示了因使用不存在的数组元素而引发的异常:


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {
            // 数组的声明与初始化
            int[] tab = { 0, 1, 2, 3 };
            int i;
            // 使用 for 循环显示数组
            for (i = 0; i < tab.Length; i++)
                Console.WriteLine("tab[{0}]={1}", i, tab[i]);
            // 使用 for each 循环显示数组
            foreach (int élmt in tab) {
                Console.WriteLine(élmt);
            }
            // 抛出异常
            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 个元素。程序运行结果如下:

tab[0]=0
tab[1]=1
tab[2]=2
tab[3]=3
0
1
2
3
L'erreur suivante s'est produite : System.IndexOutOfRangeException: L'index se trouve en dehors des limites du tableau.
   à Chap1.P08.Main(String[] args) dans C:\data\travail\2007-2008\c# 2008\poly\Chap1\08\Program.cs:第 7 行
finally ...
  • 第 9 行:发生了 [System.IndexOutOfRangeException] 异常
  • 第 11 行:代码中的 finally 子句(第 23-25 行)已被执行,尽管第 21 行中有一条 return 语句用于退出该方法。 需要注意的是,finally 子句始终会被执行。

以下是另一个示例,其中处理了将字符串赋值给整型变量时引发的异常(当该字符串不表示整数时):


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {

            // 示例 2
            // 请输入姓名
            Console.Write("Nom : ");
            // 读取回答
            string nom = Console.ReadLine();
            // 询问年龄
            int age = 0;
            bool ageOK = false;
            while (!ageOK) {
                // 提问
                Console.Write("âge : ");
                // 读取并验证回答
                try {
                    age = int.Parse(Console.ReadLine());
                    ageOK = age>=1;
                } catch {
                }//try-catch
                if (!ageOK) {
                    Console.WriteLine("Age incorrect, recommencez...");
                }
            }//while
            // 最终显示
            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行:若年龄不正确,则输出错误信息。

部分运行结果:

1
2
3
Nom : dupont
âge : 23
Vous vous appelez dupont et vous avez 23 an(s)
1
2
3
4
5
6
7
Nom : durand
âge : x
Age incorrect, recommencez...
âge : -4
Age incorrect, recommencez...
âge : 12
Vous vous appelez durand et vous avez 12 an(s)

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<=字段1 的第一行。例如,若 QF=5000,则会找到该行

    8382        0.0683        291.09

此时税额 I 等于 0.0683*R - 291.09*nbParts。 如果 QF 使得关系 QF<=field1 从未成立,则使用最后一行中的系数。此处:

0 0.4809 9505.54

由此得出税额 I=0.4809*R - 9505.54*nbParts。

相应的 C# 程序如下:


using System;

namespace Chap1 {
    class Impots {
        static void Main(string[] args) {
            // 计算税款所需的数据表
            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 };

            // 获取婚姻状况
            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";

            // 子女数量
            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

            // 工资
            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

            // 份额计算
            decimal nbParts;
            if (marie) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;

            // 应税收入
            decimal revenu = 0.72M * salaire;

            // 家庭分摊系数
            decimal QF = revenu / nbParts;

            // 查找与 QF 对应的税率档位
            int i;
            int nbTranches = limites.Length;
            limites[nbTranches - 1] = QF;
            i = 0;
            while (QF > limites[i]) i++;
            // 税额
            int impots = (int)(coeffR[i] * revenu - coeffN[i] * nbParts);

            // 显示结果
            Console.WriteLine("Impôt à payer : {0} euros", impots);
        }
    }
}
  • 第7-9行:数值后缀为M(Money),使其类型为decimal
  • 第16行:
  • Console.ReadLine() 将键盘输入的字符串 C1
  • C1.Trim() 移除 C1 首尾空格——返回字符串 C2
  • C2.ToLower() 返回字符串 C3,即字符串 C2 转换为小写后的结果。
  • 第 21 行:布尔值 marie 从关系 reponse=="o" 接收值 true false
  • 第 29 行:键盘输入的字符串被转换为类型 int。如果转换失败,将抛出异常。
  • 第 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) ? o
Nombre d'enfants : 2
Salaire annuel : 60000
Impôt à payer : 4282 euros
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 声明如下:

public static void Main(string[] args)

则将得到 args[0]="arg0", args[1]="arg1" … 以下是一个示例:


using System;

namespace Chap1 {
    class P10 {
        static void Main(string[] args) {
            // 列出接收的参数
            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] 中:[Debug] 选项卡
  • 在 [3] 中:输入参数

运行结果如下:

1
2
3
4
5
Il y a  4 arguments
arguments[0]=a0
arguments[1]=a1
arguments[2]=a2
arguments[3]=a3

请注意,签名

public static void Main()

在函数 Main 不期望任何参数的情况下是有效的。

3.8. 枚举

枚举是一种数据类型,其取值范围是一组整数常量。假设有一个程序需要管理考试的报名信息。 共有五个:PassableAssezBienBienTrèsBienExcellent

因此,我们可以为这五个常量定义一个枚举:


        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };

在内部,这五个常量由连续的整数编码,第一个常量从 0 开始,第二个从 1 开始,依此类推。可以声明一个变量来取枚举中的这些值:


            // 一个从“标注”枚举中获取值的变量
Mentions maMention = Mentions.Passable;

可以将变量与枚举中的不同可能值进行比较:


            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
}

可以获取枚举中的所有值:


            // 以字符串形式列出的标记
            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 的值列表。

如果现在编写


            //以整数形式列出标记
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
}

第 2 行,循环变量为整数类型。因此,我们将获得以整数形式表示的枚举值列表。与数据类型 Mentions 对应的 System.Type 类型对象可通过 typeof(Mentions) 获得。 也可以像之前那样写成 maMention.GetType()

以下程序演示了上述内容:


using System;

namespace Chap1 {
    class P11 {
        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };
        static void Main(string[] args) {
            // 一个从“提及”枚举中获取值的变量
            Mentions maMention = Mentions.Passable;
            // 显示变量值
            Console.WriteLine("mention=" + maMention);
            // 使用枚举值进行测试
            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
            }
            // 以字符串形式显示的标记列表
            foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
                Console.WriteLine(m);
            }
            //以整数形式显示标记列表
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
            }
        }
    }
}

执行结果如下:

mention=Passable
Peut mieux faire
Passable
AssezBien
Bien
TrèsBien
Excellent
0
1
2
3
4

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);
        }
    }
}

所得结果如下:

Paramètre formel a=30
Paramètre effectif age=20

实际参数 age 的值 20 被复制到了形式参数 a 中(第 10 行)。随后该形式参数被修改(第 11 行)。 而实际参数则保持不变。这种传递方式适用于函数的输入参数。

3.9.2. 按引用传递

在按引用传递中,实际参数和形式参数是同一个实体。如果函数修改了形式参数,实际参数也会随之改变。在 C# 中,两者都必须在前面加上 ref 关键字:

以下是一个示例:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // 示例 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);
        }
    }
}

以及运行结果:

Paramètre formel a2=30
Paramètre effectif age2=30

实际参数跟随了形式参数的修改。这种传递方式适用于函数的输出参数。

3.9.3. 使用 out 关键字的引用传递

考虑前面的示例,其中变量 age2 在调用函数 changeInt 之前未被初始化:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // 示例 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);
        }
    }
}

编译该程序时会出现错误:

    Use of unassigned local variable 'age2'

可以通过为 age2 赋初始值来解决这个问题。也可以将关键字 ref 替换为关键字 out。这样就表明该参数仅作为输出参数,因此无需初始值:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // 示例 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);
        }
    }
}

执行结果如下:

Paramètre formel a3=30
Paramètre effectif age3=30