Skip to content

5. 常用 .NET 类

本文将介绍 .NET 平台中一些常用类。在此之前,我们将展示如何获取关于数百个可用类的详细信息。即使对于经验丰富的 C# 开发人员,这些帮助也是必不可少的。 帮助文档的质量(易于访问、结构清晰、信息相关性等)可能决定开发环境的成败。

5.1. 查找 .NET 类相关帮助

本文将提供一些使用 Visual Studio.NET 查找帮助的指引

5.1.1. 帮助/目录

  • 在 [1] 中,请选择菜单中的 Help/Contents 选项。
  • 在 [2] 中,选择 Visual C# Express Edition
  • 在 [3] 中,C# 帮助树
  • 在 [4] 中,另一个有用的选项是 .NET Framework,它提供了对 .NET 框架中所有类的访问权限。

让我们浏览一下 C# 帮助中的章节标题:

  • [1]:C# 概览
  • [2]:关于 C# 某些要点的系列示例
  • [3]:C# 教程——或许可以很好地替代本文档……
  • [4]:深入探讨 C# 的细节
  • [5]:对 C++ 或 Java 开发者很有帮助。可帮助避免一些陷阱。
  • [6]:当您寻找示例时,可以从这里开始。
  • [7]:创建图形用户界面的必备知识
  • [8]:更好地使用 Visual Studio Express
  • [9]:SQL Server Express 2005 是一款免费提供的优质软件。本课程将使用该软件。

C# 帮助仅是开发人员所需内容的一部分。另一部分则是关于 .NET 框架中数百个类的帮助,这些类将极大简化开发工作。

  • [1]:选择 .NET 框架的帮助文档
  • [2]:帮助位于 .NET Framework 分支下 SDK
  • [3]:.NET Framework Class Library 分支按所属命名空间列出了所有类 .NET
  • [4]:System 命名空间,该命名空间在前几章的示例中使用最为频繁
  • [5]:位于命名空间 System 中,此处以结构 DateTime 为例
  • [6]:关于结构 DateTime 的帮助

5.1.2. Help/Index/Search

MSDN提供的帮助内容极其庞大,用户可能不知从何处查找。此时可使用帮助索引:

  • 在 [1] 中,若帮助窗口尚未打开,请使用 [Help/Index] 选项;若已打开,则在现有帮助窗口中使用 [2]。
  • 将 [3] 替换为 [4],并指定搜索范围
  • 在 [4] 中,指定要搜索的内容,此处为一个类
  • 在 [5] 中,显示结果

另一种寻求帮助的方式是使用帮助中的搜索功能:

  • 在 [1] 中,如果帮助窗口尚未打开,请使用选项 [Help/Search];否则,在已打开的帮助窗口中使用 [2]。
  • 转换为 [3],明确搜索内容
  • 在 [4] 中,筛选搜索范围
  • 在 [5] 中,以不同主题的形式显示包含所搜寻文本的搜索结果。

5.2. 字符串

5.2.1. 类 System.String

System.String 类与简单的 string 类型相同。它具有许多属性和方法。以下列举其中一些:


public int Length { get; }
nombre de caractères de la chaîne

public bool EndsWith(string value)

rend vrai si la chaîne se termine par value

public bool StartsWith(string value)

rend vrai si la chaîne commence par value
public virtual bool Equals(object obj)

rend vrai si la chaînes est égale à obj - équivalent chaîne==obj

public int IndexOf(string value, int startIndex)

rend la première position dans la chaîne de la
chaîne value - la recherche commence à partir du
caractère n° startIndex

public int IndexOf(char value, int startIndex)

idem mais pour le caractère value

public string Insert(int startIndex, string value)

insère la chaîne value dans chaîne en position
startIndex
public static string Join(string separator, string[] value)

méthode de classe - rend une chaîne de caractères,
résultat de la concaténation des valeurs du tableau
value avec le séparateur separator

public int LastIndexOf(char value, int startIndex, int count)

public int LastIndexOf(string value, int startIndex, int count)

idem indexOf mais rend la dernière position au lieu
de la première

public string Replace(char oldChar, char newChar)

rend une chaîne copie de la chaîne courante où le
caractère oldChar a été remplacé par le caractère
newChar

public string[] Split(char[] separator)

la chaîne est vue comme une suite de champs séparés
par les caractères présents dans le tableau
separator. Le résultat est le tableau de ces champs

public string Substring(int startIndex, int length)

sous-chaîne de la chaîne courante commençant à la
position startIndex et ayant length caractères
public string ToLower()
rend la chaîne courante en minuscules
public string ToUpper()
rend la chaîne courante en majuscules
public string Trim()
rend la chaîne courante débarrassée de ses espaces
de début et fin

需要注意一个重要点:当一个方法返回一个字符串时,该字符串与应用该方法的原始字符串是不同的因此,S1.Trim() 返回的字符串是 S2,而 S1S2 是两个不同的字符串。

字符串 C 可以被视为一个字符数组。因此

  • C[i] 是字符串 C 的第 i 个字符
  • C.Length 是 C 的字符数

请看以下示例:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            string uneChaine = "l'oiseau vole au-dessus des nuages";
            affiche("uneChaine=" + uneChaine);
            affiche("uneChaine.Length=" + uneChaine.Length);
            affiche("chaine[10]=" + uneChaine[10]);
            affiche("uneChaine.IndexOf(\"vole\")=" + uneChaine.IndexOf("vole"));
            affiche("uneChaine.IndexOf(\"x\")=" + uneChaine.IndexOf("x"));
            affiche("uneChaine.LastIndexOf('a')=" + uneChaine.LastIndexOf('a'));
            affiche("uneChaine.LastIndexOf('x')=" + uneChaine.LastIndexOf('x'));
            affiche("uneChaine.Substring(4,7)=" + uneChaine.Substring(4, 7));
            affiche("uneChaine.ToUpper()=" + uneChaine.ToUpper());
            affiche("uneChaine.ToLower()=" + uneChaine.ToLower());
            affiche("uneChaine.Replace('a','A')=" + uneChaine.Replace('a', 'A'));
            string[] champs = uneChaine.Split(null);
            for (int i = 0; i < champs.Length; i++) {
                affiche("champs[" + i + "]=[" + champs[i] + "]");
            }//for
            affiche("Join(\":\",champs)=" + System.String.Join(":", champs));
            affiche("(\"  abc  \").Trim()=[" + "  abc  ".Trim() + "]");
        }//主程序

        public static void affiche(string msg) {
            // 显示消息
            Console.WriteLine(msg);
        }//显示
    }//类
}//命名空间

执行后得到以下结果:

uneChaine=l'oiseau vole au-dessus des nuages
uneChaine.Length=34
chaine[10]=o
uneChaine.IndexOf("vole")=9
uneChaine.IndexOf("x")=-1
uneChaine.LastIndexOf('a')=30
uneChaine.LastIndexOf('x')=-1
uneChaine.Substring(4,7)=seau vo
uneChaine.ToUpper()=L'OISEAU VOLE AU-DESSUS DES NUAGES
uneChaine.ToLower()=l'oiseau vole au-dessus des nuages
uneChaine.Replace('a','A')=l'oiseAu vole Au-dessus des nuAges
champs[0]=[l'oiseau]
champs[1]=[vole]
champs[2]=[au-dessus]
champs[3]=[des]
champs[4]=[nuages]
Join(":",champs)=l'oiseau:vole:au-dessus:des:nuages
("  abc  ").Trim()=[abc]

再来看一个新示例:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // 待分析的行
            string ligne = "un:deux::trois:";
            // 字段分隔符
            char[] séparateurs = new char[] { ':' };
            // 拆分
            string[] champs = ligne.Split(séparateurs);
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("Champs[" + i + "]=" + champs[i]);
            }
            // 连接
            Console.WriteLine("join=[" + System.String.Join(":", champs) + "]");
        }
    }
}

以及运行结果:

1
2
3
4
5
6
Champs[0]=un
Champs[1]=deux
Champs[2]=
Champs[3]=trois
Champs[4]=
join=[un:deux::trois:]

String 类的 Split 方法可将字符串的各个元素放入数组中。此处使用的 Split 方法定义如下:


    public string[] Split(char[] separator);
separator
分隔符数组。这些字符用于分隔字符串中的各个字段。因此,如果字符串为“champ1, champ2, champ3"”,则可以使用 separator=new char[] {','}。 如果分隔符是一连串空格,则应使用 separator=null
résultat
字符串数组,其中数组的每个元素都是字符串的一个字段。

Join 方法是 String 类的静态方法:


    public static string Join(string separator, string[] value);
value
字符串数组
separator
用作字段分隔符的字符串
résultat
由数组 value 的元素连接而成,并以字符串 separator 分隔的字符串。

5.2.2. 类 System.Text.StringBuilder

此前我们提到,String 类中适用于字符串 S1 的方法会返回另一个字符串 S2System.Text.StringBuilder 类允许直接操作 S1,而无需创建字符串 S2。这通过避免生成大量生命周期极短的字符串,从而提升了性能。

该类支持多种构造函数:

StringBuilder()
constructeur par défaut
StringBuilder(String value)

construction et initialisation avec value
StringBuilder(String value, int capacité)

construction et initialisation avec value avec une taille de
bloc de capacité caractères.

一个 StringBuilder 对象使用 capacité 个字符块来存储底层字符串。默认情况下 capacité 的值为 16。上述第三个构造函数可用于指定块的容量。 存储字符串 S 所需的 capacité 字符块数量由 StringBuilder 类自动调整。 存在构造函数用于设定 StringBuilder 对象中的最大字符数。默认情况下,该最大容量为 2,147,483,647。

以下是一个说明 capacité 概念的示例:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // 字符串
            StringBuilder str = new StringBuilder("test");
            Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            for (int i = 0; i < 10; i++) {
                str.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            }
            // str2
            StringBuilder str2 = new StringBuilder("test",10);
            Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            for (int i = 0; i < 10; i++) {
                str2.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            }
        }
    }
}
  • 第 7 行:创建一个块大小为 16 个字符的 StringBuilder 对象
  • 第 8 行:str.Length 是字符串 str 的当前字符数。 str.Capacity 是当前字符串 str 在重新分配新块之前所能存储的字符数。
  • 第 10 行: str.Append(String S) 用于将类型为 String 的字符串 S 与类型为 StringBuilder 的字符串 str 进行拼接。
  • 第 14 行:创建一个块容量为 10 个字符的 StringBuilder 对象

执行结果:

taille=4, capacité=16
taille=8, capacité=16
taille=12, capacité=16
taille=16, capacité=16
taille=20, capacité=32
taille=24, capacité=32
taille=28, capacité=32
taille=32, capacité=32
taille=36, capacité=64
taille=40, capacité=64
taille=44, capacité=64
taille=4, capacité=10
taille=8, capacité=10
taille=12, capacité=20
taille=16, capacité=20
taille=20, capacité=20
taille=24, capacité=40
taille=28, capacité=40
taille=32, capacité=40
taille=36, capacité=40
taille=40, capacité=40
taille=44, capacité=80

这些结果表明,当内存容量不足时,该类会遵循其特有的算法来分配新的内存块:

  • 第 4-5 行:容量增加 16 个字符
  • 第 8-9 行:将容量增加 32 个字符,而实际上增加 16 个字符就已足够。

以下是该类的一些方法:


public StringBuilder Append(string value)

ajoute la chaîne value à l'objet StringBuilder. Rend
l'objet StringBuilder. Cette méthode est surchargée
 pour admettre différents types pour value : byte,
int, float, double, decimal, ... 

public StringBuilder Insert(int index,
string value)

insère value à la position index. Cette méthode est
surchargée comme la précédente pour accepter
différents types pour value.

public StringBuilder Remove(int index, int length)

supprime length caractères à partir de la position
index.

public StringBuilder Replace(string oldValue,
string newValue)

remplace dans StringBuilder, la chaîne oldValue par
la chaîne newValue. Il existe une version surchargée
(char oldChar, char newChar).
public String ToString()

convertit l'objet StringBuilder en un objet de type
String.

以下是一个示例:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // str3
            StringBuilder str3 = new StringBuilder("test");
            Console.WriteLine(str3.Append("abCD").Insert(2, "xyZT").Remove(0, 2).Replace("xy", "XY"));
        }
    }
}

及其结果:

XYZTstabCD

5.3. 数组

数组继承自 Array 类:

Array 类拥有多种方法,用于对数组进行排序、在数组中查找元素、调整数组大小等。我们将介绍该类的一些属性和方法。它们几乎都被重载了,c.a.d,因此存在多种变体。所有数组都继承了这些方法。

属性

public int Length {get;}
nombre total d'éléments du tableau, quelque soit son nombre de dimensions
public int Rank {get;}
nombre total de dimensions du tableau

方法

public static int BinarySearch<T>(T[] tableau,
 value)
rend la position de [value] dans tableau.
public static int BinarySearch<T>(T[] tableau,
nt index, int length, T value)
idem mais cherche dans tableau à partir de la
position [index] et sur [length] éléments
public static void Clear(Array tableau, int index,
int length)
met les [length] éléments de tableau commençant au
 n° [index] à 0 si numériques, false si booléens, null si références
public static void Copy(Array source,
Array destination, int length)
copie [length] éléments de source dans destination
public int GetLength(int i)
nombre d'éléments de la dimension n° i du tableau
public int GetLowerBound(int i)
indice du 1er élément de la dimension n° i
public int GetUpperBound(int i)
indice du dernier élément de la dimension n° i
public static int IndexOf<T>(T[] tableau,
T valeur)
rend la position de valeur dans tableau ou -1 si
valeur n'est pas trouvée.
public static void Resize<T>(ref T[] tableau,
int n)
redimensionne tableau à n éléments. Les éléments
déjà présents sont conservés.
public static void Sort<T>(T[] tableau,
IComparer<T> comparateur)
trie tableau selon un ordre défini par comparateur.
Cette méthode a été présentée au paragraphe Erreur : source de la référence non trouvée.

以下程序演示了 Array 类中某些方法的使用:


using System;

namespace Chap3 {
    class Program {
        // 搜索类型
        enum TypeRecherche { linéaire, dichotomique };

        // 主要方法
        static void Main(string[] args) {
            // 读取通过键盘输入的数组元素
            double[] éléments;
            Saisie(out éléments);
            // 显示未排序数组
            Affiche("Tableau non trié", éléments);
            // 在未排序数组中进行线性搜索
            Recherche(éléments, TypeRecherche.linéaire);
            // 数组排序
            Array.Sort(éléments);
            // 显示已排序数组
            Affiche("Tableau trié", éléments);
            // 在已排序数组中进行二分搜索
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // 输入数组元素的值
        // 元素:引用由该方法创建的数组
        static void Saisie(out double[] éléments) {
            bool terminé = false;
            string réponse;
            bool erreur;
            double élément = 0;
            int i = 0;
            // 初始时数组不存在
            éléments = null;
            // 数组元素输入循环
            while (!terminé) {
                // 问题
                Console.Write("Elément (réel) " + i + " du tableau (rien pour terminer) : ");
                // 读取回答
                réponse = Console.ReadLine().Trim();
                // 若字符串为空则结束输入
                if (réponse.Equals(""))
                    break;
                // 验证输入
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.Error.WriteLine("Saisie incorrecte, recommencez");
                    erreur = true;
                }//try-catch
                // 若无错误
                if (!erreur) {
                    // 数组中增加一个元素
                    i += 1;
                    // 调整数组大小以容纳新元素
                    Array.Resize(ref éléments, i);
                    // 插入新元素
                    éléments[i - 1] = élément;
                }
            }//while
        }

        // 用于显示数组元素的通用方法
        static void Affiche<T>(string texte, T[] éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // 在数组中查找元素
        // 元素:实数数组
        // TypeRecherche:二分法或线性
        static void Recherche(double[] éléments, TypeRecherche type) {
            // 搜索
            bool terminé = false;
            string réponse = null;
            double élément = 0;
            bool erreur = false;
            int i = 0;
            while (!terminé) {
                // 问题
                Console.WriteLine("Elément cherché (rien pour arrêter) : ");
                // 阅读并核对答案
                réponse = Console.ReadLine().Trim();
                // 结束了吗?
                if (réponse.Equals(""))
                    break;
                // 验证
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.WriteLine("Erreur, recommencez...");
                    erreur = true;
                }//try-catch
                // 如果没有错误
                if (!erreur) {
                    // 在数组中查找元素
                    if (type == TypeRecherche.dichotomique)
                        // 二分搜索
                        i = Array.BinarySearch(éléments, élément);
                    else
                        // 线性搜索
                        i = Array.IndexOf(éléments, élément);
                    // 显示结果
                    if (i >= 0)
                        Console.WriteLine("Trouvé en position " + i);
                    else
                        Console.WriteLine("Pas dans le tableau");
                }//if
            }//while
        }
    }
}
  • 第 27-62 行:方法 Saisie 用于将通过键盘输入的元素存入数组 éléments 中。由于无法预先确定数组的大小(其最终大小未知),因此必须在添加每个新元素时重新调整数组大小 (第57行)。更高效的算法本应是按N个元素为一组为数组分配空间。然而,数组本身并不适合进行动态调整大小。这种情况更适合使用列表(ArrayList, List<T>)来处理。
  • 第75-113行:方法Recherche用于在数组éléments中搜索用户键入的元素。 搜索模式取决于数组是否已排序。对于未排序的数组,使用第106行的方法 IndexOf 进行线性搜索。对于已排序的数组,使用第103行的方法 BinarySearch 进行二分搜索。
  • 第18行:对数组éléments进行排序。此处使用的是Sort的一个变体,该方法仅有一个参数:待排序的数组。 用于比较数组元素的排序关系即为这些元素的隐含关系。此处元素为数值类型,因此采用数字的自然排序顺序。

屏幕输出结果如下:

Elément (réel) 0 du tableau (rien pour terminer) : 3,6
Elément (réel) 1 du tableau (rien pour terminer) : 7,4
Elément (réel) 2 du tableau (rien pour terminer) : -1,5
Elément (réel) 3 du tableau (rien pour terminer) : -7
Elément (réel) 4 du tableau (rien pour terminer) :
Tableau non trié----------------------------------
3,6
7,4
-1,5
-7
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 1
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

Tableau trié--------------------------------------
-7
-1,5
3,6
7,4
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 3
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

5.4. 泛型集合

除了数组之外,还有各种类用于存储元素集合。System.Collections.Generic 命名空间中提供了泛型版本,而 System.Collections 命名空间中则提供了非泛型版本。我们将介绍两种常用的泛型集合:列表和字典

泛型集合列表如下:

Image

5.4.1. 泛型类 List<T>

System.Collections.Generic.List<T> 类允许实现由类型 T 的对象组成的集合,其大小在程序运行过程中会发生变化。 List<T> 类型的对象操作方式几乎与数组相同。因此,列表 l 的第 i 个元素记为 l[i]。

此外还存在一种非泛型列表类型:ArrayList,能够存储指向任意对象的引用。 ArrayList 在功能上等同于 List<Object>.一个 ArrayList 对象看起来像这样:

在上例中,列表中的元素 0、1 和 i 分别指向不同类型的对象。 必须先创建一个对象,然后才能将其引用添加到列表 ArrayList 中。尽管 ArrayList 存储对象引用,但也可以在其中存储数字。 这通过一种名为 Boxing 的机制实现:数字被封装在一个类型为 Object 的对象 O 中,而列表中存储的是对象 O 的引用。这对开发人员来说是一个透明的机制。因此可以这样编写:

ArrayList liste=new ArrayList();
liste.Add(4);

这将产生以下结果:

在上例中,数字 4 被封装在一个 O 对象中,而 O 引用被存储在列表中。要检索它,可以这样写:


            int i = (int)liste[0];

操作 Object -> int 称为 Unboxing。如果一个列表完全由 int 类型组成,将其声明为 List<int> 可以提高性能。 因为这样,int类型的数值将直接存储在列表内部,而非存储在列表外部的Object类型中。因此不再需要进行装箱/拆箱操作。

对于 List<T>T 类型的对象(即类),列表同样存储 T 类型对象的引用:

以下是泛型列表的一些属性和方法:

属性

public int Count {get;}
nombre d'éléments de la liste
public int Capacity {get;}
nombre d'éléments que la liste peut contenir avant d'être redimensionnée. Ce
redimensionnement se fait automatiquement. Cette notion de capacité de liste
est analogue à celle de capacité décrite pour la classe StringBuilder paragraphe Erreur : source de la référence non trouvée.

方法

public void Add(T item)
ajoute item à la liste
public int BinarySearch<T>(T item)
rend la position de item dans la liste s'il s'y
trouve sinon un nombre <0
public int BinarySearch<T>(T item,
IComparer<T> comparateur)
idem mais le 2ième paramètre permet de comparer deux
éléments de la liste. L'interface IComparer<T> a été
présentée au paragraphe Erreur : source de la référence non trouvée.
public void Clear()
supprime tous les éléments de la liste
public bool Contains(T item)
rend True si item est dans la liste, False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public int IndexOf(T item)
rend la position de item dans tableau ou -1 si
valeur n'est pas trouvée.
public void Insert(T item, int index)
insère item à la position index de la liste
public bool Remove(T item)
supprime item de la liste. Rend True si l'opération
réussit, False sinon.
public void RemoveAt(int index)
supprime l'élément n° index de la liste
public void Sort(IComparer<T> comparateur)
trie la liste selon un ordre défini par comparateur.
 Cette méthode a été présentée paragraphe Erreur : source de la référence non trouvée.
public void Sort()
trie la liste selon l'ordre défini par le type des
éléments de la liste
public T[] ToArray()
rend les éléments de la liste sous forme de tableau

让我们重新审视之前处理过的示例,当时使用的对象类型为 Array,现在我们改用 List<T>. 类型的对象进行处理。由于列表是一种类似数组的对象,代码变化不大。我们仅列出显著的修改:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        // 搜索类型
        enum TypeRecherche { linéaire, dichotomique };

        // 主方法
        static void Main(string[] args) {
            // 读取通过键盘输入的列表项
            List<double> éléments;
            Saisie(out éléments);
            // 元素数量
            Console.WriteLine("La liste a {0} éléments et une capacité de {1} éléments", éléments.Count, éléments.Capacity);
            // 显示未排序列表
            Affiche("Liste non triée", éléments);
            // 在未排序列表中进行线性搜索
            Recherche(éléments, TypeRecherche.linéaire);
            // 列表排序
            éléments.Sort();
            // 显示已排序列表
            Affiche("Liste triée", éléments);
            // 在已排序列表中进行二分搜索
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // 输入元素列表的值
        // 元素:对由该方法创建的列表的引用
        static void Saisie(out List<double> éléments) {
...
            // 初始时列表为空
            éléments = new List<double>();
            // 列表元素输入循环
            while (!terminé) {
...
                // 若无错误
                if (!erreur) {
                    // 列表中增加一个元素
                    éléments.Add(élément);
                }
            }//while
        }

        // 用于显示可枚举对象元素的通用方法
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // 在列表中查找元素
        // 元素:实数列表
        // TypeRecherche:二分法或线性
        static void Recherche(List<double> éléments, TypeRecherche type) {
...
            while (!terminé) {
...
                // 若无错误
                if (!erreur) {
                    // 在列表中查找该元素
                    if (type == TypeRecherche.dichotomique)
                        // 二分搜索
                        i = éléments.BinarySearch(élément);
                    else
                        // 线性搜索
                        i = éléments.IndexOf(élément);
                    // 显示答案
...
                }//if
            }//while
        }
    }
}
  • 第 46-51 行:通用方法 Affiche<T> 接受两个参数:
  • 第一个参数是要写入的文本
  • 第二个参数是实现泛型接口 IEnumerable<T> 的对象:
1
2
3
4
public interface IEnumerable<T>{
    IEnumerator GetEnumerator();
    IEnumerator<T> GetEnumerator();
}

第 48 行中的 foreach( T 元素 in 元素列表) 结构,对所有实现 IEnumerable 接口的 éléments 对象均有效。 数组(Array)和列表(List<T>)都实现了接口 IEnumerable<T>。 因此,方法 Affiche 同样适用于显示表格和列表。

该程序的运行结果与使用类 Array 的示例相同。

5.4.2. 类 Dictionary<TKey,TValue>

System.Collections.Generic.Dictionary<TKey,TValue> 用于实现字典。可以将字典视为一个两列数组:

键1
值1
键2
值2
..
...

Dictionary<TKey,TValue> 类中,键的类型为 Tkey,值的类型为 TValue。 键是唯一的,例如 c.a.d,因此不可能存在两个相同的键。如果类型 TKeyTValue 代表类,那么这样的字典可能如下所示:

字典 D 中键 C 对应的值通过 D[C] 表示。该值支持读写操作。因此可以写入:

1
2
3
4
5
TValue v=...;
TKey c=...;
Dictionary<TKey,TValue> D=new Dictionary<TKey,TValue>();
D[c]=v;
v=D[c];

如果键 c 在字典 D 中不存在,则表示法 D[c] 将引发异常。

Dictionary<TKey,TValue> 的主要方法和属性如下:

构造函数

public Dictionary<TKey,TValue>()
constructeur sans paramètres - construit un dictionnaire vide.
Il existe plusieurs autres constructeurs.

属性

public int Count {get;}
nombre d'entrées (clé, valeur) dans le dictionnaire
public Dictionary<TKey,TValue>.KeyCollection Keys {get;}
collection des clés du dictionnaire.
public Dictionary<TKey,TValue>.ValueCollection Values {get;}
collection des valeurs du dictionnaire.

方法

public void Add(TKey key, TValue value)
ajoute le couple (key, value) au dictionnaire
public void Clear()
supprime tous les couples du dictionnaire
public bool ContainsKey (TKey key)
rend True si key est une clé du dictionnaire,
False sinon
public bool ContainsValue (TValue value)
rend True si value est une valeur du dictionnaire,
False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public bool Remove(TKey key)
supprime du dictionnaire le couple de clé key.
Rend True si l'opération réussit, False sinon.
public bool TryGetValue(TKey key,
out TValue value)
rend dans value, la valeur associée à la clé key si
cette dernière existe, sinon rend la valeur par
défaut du type TValue (0 pour les nombres, false
pour les booléens, null pour les références d'objet)

请看以下示例程序:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // 创建 <string,int> 字典
            string[] liste = { "jean:20", "paul:18", "mélanie:10", "violette:15" };
            string[] champs = null;
            char[] séparateurs = new char[] { ':' };
            Dictionary<string,int> dico = new Dictionary<string,int>();
            for (int i = 0; i <liste.Length; i++) {
                champs = liste[i].Split(séparateurs);
                dico[champs[0]]= int.Parse(champs[1]);
            }//for
            // 字典中的元素数量
            Console.WriteLine("Le dictionnaire a " + dico.Count + " éléments");
            // 键列表
            Affiche("[Liste des clés]",dico.Keys);
            // 值列表
            Affiche("[Liste des valeurs]", dico.Values);
            // 键值对列表
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // 删除键“paul”
            Console.WriteLine("[Suppression d'une clé]");
            dico.Remove("paul");
            // 键值对列表
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // 在字典中搜索
            String nomCherché = null;
            Console.Write("Nom recherché (rien pour arrêter) : ");
            nomCherché = Console.ReadLine().Trim();
            int value;
            while (!nomCherché.Equals("")) {
                dico.TryGetValue(nomCherché, out value);
                if (value!=0) {
                    Console.WriteLine(nomCherché + "," + value);
                } else {
                    Console.WriteLine("Nom " + nomCherché + " inconnu");
                }
                // 后续搜索
                Console.Out.Write("Nom recherché (rien pour arrêter) : ");
                nomCherché = Console.ReadLine().Trim();
            }//while
        }

        // 用于显示可枚举类型元素的通用方法
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

    }
}
  • 第 8 行:一个 string 数组,用于初始化 <string,int> 字典
  • 第 11 行:<string,int> 字典
  • 第12-15行:基于第8行的string数组对其进行初始化
  • 第17行:字典的条目数
  • 第 19 行:字典的键
  • 第 21 行:字典的值
  • 第 29 行:从字典中删除一个条目
  • 第41行:在字典中查找键。如果不存在,方法TryGetValue将向value赋值0,因为value是数值型。 此处之所以能使用该技术,是因为已知字典中不存在值 0。

执行结果如下:

Le dictionnaire a 4 éléments
[Liste des clés]----------------------------------
jean
paul
mélanie
violette
[Liste des valeurs]-------------------------------
20
18
10
15
[Liste des clés & valeurs]
clé=jean valeur=20
clé=paul valeur=18
clé=mélanie valeur=10
clé=violette valeur=15
[Suppression d'une clé]
[Liste des clés & valeurs]
clé=jean valeur=20
clé=mélanie valeur=10
clé=violette valeur=15
Nom recherché (rien pour arrêter) : violette
violette,15
Nom recherché (rien pour arrêter) : x
Nom x inconnu

5.5. 文本文件

5.5.1. StreamReader 类

System.IO.StreamReader 类用于读取文本文件的内容。实际上,它也能处理非文件流。以下是该类的一些属性和方法:

构造函数

public StreamReader(string path)
construit un flux de lecture à partir du fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

属性

public bool EndOfStream {get;}
True si le flux a été lu entièrement

方法

public void Close()
ferme le flux et libère les ressources allouées pour
sa gestion. A faire obligatoirement après
exploitation du flux.
public override int Peek()
rend le caractère suivant du flux sans le consommer.
Un Peek supplémentaire rendrait donc le même
caractère.
public override int Read()
rend le caractère suivant du flux et avance d'un
caractère dans le flux.
public override int Read(char[] buffer,
int index, int count)
lit count caractères dans le flux et les met dans
buffer à partir de la position index. Rend le nombre
de caractères lus - peut être 0.
public override string ReadLine()
rend la ligne suivante du flux ou null si on était à
la fin du flux.
public override string ReadToEnd()
rend la fin du flux ou "" si on était à la fin du
flux.

以下是一个示例:


using System;
using System.IO;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // 运行目录
            Console.WriteLine("Répertoire d'exécution : "+Environment.CurrentDirectory);
            string ligne = null;
            StreamReader fluxInfos = null;
            // 读取文件内容infos.txt
            try {
                // 读取 1
                Console.WriteLine("Lecture 1----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    ligne = fluxInfos.ReadLine();
                    while (ligne != null) {
                        Console.WriteLine(ligne);
                        ligne = fluxInfos.ReadLine();
                    }
                }
                // 读取 2
                Console.WriteLine("Lecture 2----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    Console.WriteLine(fluxInfos.ReadToEnd());
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • 第 8 行:显示执行目录的名称
  • 第12、27行:用于处理可能出现的异常的try/catch语句。
  • 第 15 行:using 语句的结构 flux=new StreamReader(...) 提供了一种便利,避免在流处理完成后必须显式关闭流。只要退出 using 的作用域,该流就会自动关闭。
  • 第 15 行:读取的文件名为 infos.txt。由于这是一个相对路径,系统将根据第 8 行显示的执行目录进行查找。若该文件不存在,将抛出异常并由 try/catch 进行处理。
  • 第16-20行:按行依次读取文件
  • 第25行:一次性读取整个文件

文件 infos.txt 的内容如下:

12620:0:0
13190:0,05:631
15640:0,1:1290,5

并放置在 C# 项目的以下文件夹中:

我们将发现,当通过 Ctrl-F5 运行项目时,bin/Release 即是运行文件夹。

运行后得到以下结果:

1
2
3
4
5
6
7
8
9
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5
Lecture 2----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5

如果在第15行输入文件名 xx.txt,将得到以下结果:

1
2
3
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
L'erreur suivante s'est produite : Could not find file 'C:\...\Chap3\07\bin\Release\xx.txt'.

5.5.2. 类 StreamWriter

System.IO.StreamReader 用于向文本文件写入数据。与类 StreamReader 一样,它实际上也能处理非文件流。以下是该类的一些属性和方法:

构造函数

public StreamWriter(string path)
construit un flux d'écriture dans le fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

属性

public virtual bool AutoFlush
{get;set;}
fixe le mode d'écriture dans le fichier du buffer associé au flux. Si
égal à False, l'écriture dans le flux n'est pas immédiate : il y a
d'abord écriture dans une mémoire tampon puis dans le fichier lorsque la
mémoire tampon est pleine sinon l'écriture dans le fichier est immédiate
(pas de tampon intermédiaire). Par défaut c'est le mode tamponné qui est
utilisé. Le tampon n'est écrit dans le fichier que lorsqu'il est plein ou
bien lorsqu'on le vide explicitement par une opération Flush ou encore
lorsqu'on ferme le flux StreamWriter par une opération Close. Le mode
AutoFlush=False est le plus efficace lorsqu'on travaille avec des
fichiers parce qu'il limite les accès disque. C'est le mode par défaut
pour ce type de flux. Le mode AutoFlush=False ne convient pas à tous les
flux, notamment les flux réseau. Pour ceux-ci, qui souvent prennent place
dans un dialogue entre deux partenaires, ce qui est écrit par l'un des
partenaires doit être immédiatement lu par l'autre. Le flux d'écriture
doit alors être en mode AutoFlush=True.
public virtual string NewLine {get;set;}
les caractères de fin de ligne. Par défaut "\r\n". Pour un système Unix,
il faudrait utiliser "\n".

方法

public void Close()
ferme le flux et libère les ressources allouées pour sa
gestion. A faire obligatoirement après exploitation du flux.
public override void Flush()
écrit dans le fichier, le buffer du flux, sans attendre qu'il
soit plein.
public virtual void Write(T value)
écrit value dans le fichier associé au flux. Ici T n'est pas
un type générique mais symbolise le fait que la méthode
Write accepte différents types de paramètres (string, int,
object, ...). La méthode value.ToString est utilisée pour
produire la chaîne écrite dans le fichier.
public virtual void WriteLine(T value)
même chose que Write mais avec la marque de fin de ligne
(NewLine) en plus.

请看以下示例:


using System;
using System.IO;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // 执行目录
            Console.WriteLine("Répertoire d'exécution : " + Environment.CurrentDirectory);
            string ligne = null;                        // 一行文本
            StreamWriter fluxInfos = null;    // 文本文件
            try {
                // 创建文本文件
                using (fluxInfos = new StreamWriter("infos2.txt")) {
                    Console.WriteLine("Mode AutoFlush : {0}", fluxInfos.AutoFlush);
                    // 读取键盘输入的行
                    Console.Write("ligne (rien pour arrêter) : ");
                    ligne = Console.ReadLine().Trim();
                    // 只要输入的行不为空就循环
                    while (ligne != "") {
                        // 将行写入文本文件
                        fluxInfos.WriteLine(ligne);
                        // 读取键盘输入的新行
                        Console.Write("ligne (rien pour arrêter) : ");
                        ligne = Console.ReadLine().Trim();
                    }//while
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • 第 13 行:我们再次使用 using(flux) 语法,这样就无需通过 Close 操作显式关闭流。该关闭操作会在 using 结束时自动执行。
  • 为什么第11行和第27行要使用try/catch?第13行,我们可以指定一个文件名,格式为 /rep1/rep2/ .../fichier 时,若路径 /rep1/rep2/... 不存在,将导致无法创建 fichier。此时会抛出异常。此外还可能存在其他异常情况(磁盘已满、权限不足等)

执行结果如下:

1
2
3
4
5
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Mode AutoFlush : False
ligne (rien pour arrêter) : 1ère ligne
ligne (rien pour arrêter) : 2ième ligne
ligne (rien pour arrêter) :

文件 infos2.txt 已创建在项目 bin/Release 的文件夹中:

 

5.6. 二进制文件

System.IO.BinaryReaderSystem.IO.BinaryWriter 用于读取和写入二进制文件。

考虑以下应用程序:

// pg 语法 文本 二进制 日志
// 读取文本文件(text),并将内容存储到二进制文件(bin)中
// 该文本文件包含形式为“姓名:年龄”的行,我们将这些行存储在字符串和整数结构中
// (logs) 是一个日志文本文件

该文本文件的内容如下:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

程序如下:


using System;
using System.IO;

// 语法:pg 文本 二进制 日志
// 读取文本文件(text),并将内容存储到二进制文件(bin)中
// 该文本文件包含形式为“姓名:年龄”的行,将这些行存储在字符串和整数结构中
// (logs) 是一个日志文本文件

namespace Chap3 {
    class Program {
        static void Main(string[] arguments) {
            // 需要 3 个参数
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg texte binaire log");
                Environment.Exit(1);
            }//if

            // 变量
            string ligne=null;
            string nom=null;
            int age=0;
            int numLigne = 0;
            char[] séparateurs = new char[] { ':' };
            string[] champs=null;
            StreamReader input = null;
            BinaryWriter output = null;
            StreamWriter logs = null;
            bool erreur = false;
            // 读取文本文件 - 写入二进制文件
            try {
                // 以读取模式打开文本文件
                input = new StreamReader(arguments[0]);
                // 以写入模式打开二进制文件
                output = new BinaryWriter(new FileStream(arguments[1], FileMode.Create, FileAccess.Write));
                // 以写入模式打开日志文件
                logs = new StreamWriter(arguments[2]);
                // 处理文本文件
                while ((ligne = input.ReadLine()) != null) {
                    // 多一行
                    numLigne++;
                    // 空行?
                    if (ligne.Trim() == "") {
                        // 忽略
                        continue;
                    }
                    // 一行:姓名:年龄
                    champs = ligne.Split(séparateurs);
                    // 我们需要 2 个字段
                    if (champs.Length != 2) {
                        // 记录错误
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nombre de champs incorrect", numLigne, arguments[0]);
                        // 下一行
                        continue;
                    }//if
                    // 第一个字段不能为空
                    erreur = false;
                    nom = champs[0].Trim();
                    if (nom == "") {
                        // 记录错误
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nom vide", numLigne, arguments[0]);
                        erreur = true;
                    }
                    // 第二个字段必须为大于等于0的整数
                    if (!int.TryParse(champs[1],out age) || age<0) {
                        // 记录错误
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un âge [{2}] incorrect", numLigne, arguments[0], champs[1].Trim());
                        erreur = true;
                    }//if
                    // 若无错误,将数据写入二进制文件
                    if (!erreur) {
                        output.Write(nom);
                        output.Write(age);
                    }
                    // 下一行
                }//while
            }catch(Exception e){
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // 关闭文件
                if(input!=null) input.Close();
                if(output!=null) output.Close();
                if(logs!=null) logs.Close();
            }
        }
    }
}

让我们重点关注与类 BinaryWriter 相关的操作:

  • 第 34 行:操作打开了对象 BinaryWriter

            output=new BinaryWriter(new FileStream(arguments[1],FileMode.Create,FileAccess.Write));

构造函数的参数必须是一个流(Stream)。这里是一个由文件(FileStream)构建的流,其内容如下:

  • (续)
    • 文件名
    • 要执行的操作,此处为 FileMode.Create,用于创建文件
    • 访问类型,此处为 FileAccess.Write,表示对文件的写入访问
  • 第70-73行:写入操作
             // 将数据写入二进制文件
            output.Write(nom);
            output.Write(age);

BinaryWriter 提供了多种重载方法 Write,用于写入不同类型的简单数据

  • 第 81 行:关闭数据流的操作
        output.Close();

方法 Main 的三个参数通过项目属性传递给 [1],待处理的文本文件位于 bin/Release 文件夹中 [2]:

以及以下文件 [personnes1.txt]:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

执行结果如下:

  • 在 [1] 中,生成了二进制文件 [personnes1.bin] 以及日志文件 [logs.txt]。该日志文件内容如下:
1
2
3
La ligne n° [6] du fichier [personnes1.txt] a un âge [-1] incorrect
La ligne n° [8] du fichier [personnes1.txt] a un nombre de champs incorrect
La ligne n° [9] du fichier [personnes1.txt] a un âge [yy] incorrect

二进制文件 [personnes1.bin] 的内容将由以下程序生成。该程序同样接受三个参数:

// 语法 pg bin 文本 日志
// 读取二进制文件 bin 并将内容保存到文本文件(文本)中
// 二进制文件具有字符串、整数结构
// 文本文件中的行格式为 姓名 : 年龄
// logs 是一个日志文本文件

因此,我们进行反向操作。读取二进制文件以生成文本文件。如果生成的文本文件与原始文件完全一致,则说明文本→二进制→文本的转换过程已成功完成。代码如下:


using System;
using System.IO;

// 语法:pg bin 文本 logs
// 读取二进制文件 bin 并将内容保存到文本文件(文本)中
// 二进制文件具有字符串、整数结构
// 文本文件中的行格式为:姓名 : 年龄
// logs 是一个日志文本文件

namespace Chap3 {
    class Program2 {
        static void Main(string[] arguments) {
            // 需要 3 个参数
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg binaire texte log");
                Environment.Exit(1);
            }//if

            // 变量
            string nom = null;
            int age = 0;
            int numPersonne = 1;
            BinaryReader input = null;
            StreamWriter output = null;
            StreamWriter logs = null;
            bool fini;
            // 读取二进制文件 - 写入文本文件
            try {
                // 以读取模式打开二进制文件
                input = new BinaryReader(new FileStream(arguments[0], FileMode.Open, FileAccess.Read));
                // 以写入模式打开文本文件
                output = new StreamWriter(arguments[1]);
                // 以写入模式打开日志文件
                logs = new StreamWriter(arguments[2]);
                // 二进制文件处理
                fini = false;
                while (!fini) {
                    try {
                        // 读取名称
                        nom = input.ReadString().Trim();
                        // 读取年龄
                        age = input.ReadInt32();
                        // 向文本文件写入
                        output.WriteLine(nom + ":" + age);
                        // 下一位
                        numPersonne++;
                    } catch (EndOfStreamException) {
                        fini = true;
                    } catch (Exception e) {
                        logs.WriteLine("L'erreur suivante s'est produite à la lecture de la personne n° {0} : {1}", numPersonne, e.Message);
                    }
                }//while
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // 关闭文件
                if (input != null)
                    input.Close();
                if (output != null)
                    output.Close();
                if (logs != null)
                    logs.Close();
            }
        }
    }
}

让我们重点关注与类 BinaryReader 相关的操作:

  • 第30:通过操作

            input=new BinaryReader(new FileStream(arguments[0],FileMode.Open,FileAccess.Read));

构造函数的参数必须是一个流(Stream)。此处是一个由文件(FileStream)构建的流,其内容如下:

  • (续)
    • 名称
    • 要执行的操作,此处FileMode.Open,用于打开现有文件
    • 访问类型,此处为 FileAccess.Read,表示对文件的读取访问
  • 第40、42行:读取操作
nom=input.ReadString().Trim();
age=input.ReadInt32();

BinaryReader 提供了多种方法 ReadXX 用于读取不同类型的简单数据

  • 第 60 行:关闭流的操作
        input.Close();

如果按顺序执行这两个程序,将 personnes1.txt 转换为 personnes1.bin,然后将 personnes1.bin 转换为 personnes2.txt2,将得到以下结果:

  • 在 [1] 中,该项目配置为运行第二个应用程序
  • 在 [2] 中,传递给 Main 的参数
  • 在 [3] 中,是应用程序运行生成的文件。

[personnes2.txt] 的内容如下:

1
2
3
4
paul:10
helene:15
jacques:11
sylvain:12

5.7. 正则表达式

System.Text.RegularExpressions.Regex 支持使用正则表达式。这些表达式可用于验证字符串的格式。因此,我们可以验证表示日期的字符串是否确实采用 dd/mm/yy 格式。为此,我们使用一个模式并将字符串与该模式进行比较。因此,在此示例中,j、m 和 a 必须是数字。 因此,有效的日期格式模式为“\d\d/\d\d/\d\d”,其中符号 \d 表示数字。模式中可使用的符号如下:

Caractère
描述
\ 
将下一个字符标记为特殊字符或字面字符。例如,"n" 表示字符 "n"。"\n" 表示换行符。字符序列 "\\" 表示 "\",而 "\(" 表示 "("。
^ 
表示输入的开头。
$ 
表示输入结束。
* 
表示前一个字符零次或多次出现。因此,“zo*”表示“z”或“zoo”。
+ 
匹配前一个字符一次或多次。因此,“zo+”匹配“zoo”,但不匹配“z”。
? 
匹配前一个字符零次或一次。例如,“a?ve?”匹配“lever”中的“ve”。
.
匹配除换行符以外的任何单个字符。
(modèle) 
搜索 modèle 并记录匹配结果。 可以通过 Item [0]...[n] 从获得的 Matches 集合中提取匹配的子字符串。要查找包含括号 ( ) 内的字符的匹配项,请使用 "\(" 或 "\)"。
x|y
匹配 xy。例如,“z|foot”匹配“z”或“foot”。“(z|f)oo”匹配“zoo”或“foo”。
{n}
n 是一个非负整数。精确匹配 n 倍的该字符。例如,“o{2}”不匹配“Bob”中的“o”,而是匹配“fooooot”中的前两个“o”。
{n,} 
n 是一个非负整数。匹配该字符至少 n 次。 例如,“o{2,}”不匹配“Bob”中的“o”,而是匹配“fooooot”中的所有“o”。“o{1,}”等同于“o+”,而“o{0,}”等同于“o*”。
{n,m} 
m n 是非负整数。 表示该字符出现次数不少于 n 次,且不超过 m 次。例如,“o{1,3}”匹配“foooooot”中的前三个“o”,而“o{0,1}”等同于“o?”。
[xyz] 
字符集。匹配所列出的任意一个字符。例如,“[abc]”匹配“plat”中的“a”。
[^xyz] 
否定字符集。匹配所有未列出的字符。例如,“[^abc]”匹配“plat”中的“p”。
[a-z] 
字符范围。匹配指定字符序列中的任何字符。例如,“[a-z]”匹配“a”到“z”之间的所有小写字母。
[^m-z] 
负字符集。匹配指定字符集中的所有非字符。例如,“[^m-z]”匹配“m”和“z”之间的所有非字符。
\b 
匹配表示单词的边界,即单词与空格之间的位置。例如,“er\b”匹配“lever”中的“er”,但不匹配“verbe”中的“er”。
\B 
对应不代表单词的边界。“en*t\B”对应“bien entendu”中的“ent”。
\d 
匹配代表数字的字符。等同于 [0-9]。
\D 
对应于不代表数字的字符。等同于 [^0-9]。
\f 
对应于换行符。
\n 
对应换行符。
\r 
对应回车符。
\s 
对应所有空白字符,包括空格、制表符、换行符等。等同于“[ \f\n\r\t\v]”。
\S 
匹配所有非空白字符。等同于“[^ \f\n\r\t\v]”。
\t 
对应一个制表符。
\v 
对应垂直制表符。
\w 
对应于任何代表单词且包含下划线的字符。等同于“[A-Za-z0-9_]”。
\W 
对应于任何不代表单词的字符。等同于“[^A-Za-z0-9_]”。
\num 
对应于 num,其中 num 是一个正整数。指代存储的映射关系。例如,"(.)\1" 对应于两个连续的相同字符。
\n
对应于 n,其中 n 是一个八进制转义值。八进制转义值必须包含 1、2 或 3 个数字。例如,"\11" 和 "\011" 都表示一个制表符。 "\0011" 等同于 "\001" & "1"。八进制转义值不得超过 256。如果超过,则表达式中仅考虑前两位数字。允许在正则表达式中使用 ASCII 代码。
\xn
对应于 n,其中 n 是一个十六进制转义值。 十六进制转义值必须包含两个数字。例如,“\x41”对应“A”。“\x041”等同于“\x04”和“1”。允许在正则表达式中使用代码 ASCII。

模板中的一个元素可以出现一次或多次。下面通过几个关于 \d 符号的示例来说明,该符号代表 1 个数字:

模板
含义
\d
一个数字
\d?
0 或 1 个数字
\d*
0 个或更多数字
\d+
1 个或多个数字
\d{2}
2个数字
\d{3,}
至少 3 个数字
\d{5,7}
5 到 7 位数字

现在设想一个能够描述字符串预期格式的模式:

要查找的字符串
模式
日期格式为 dd/mm/yy
\d{2}/\d{2}/\d{2}
时长格式为 hh:mm:ss
\d{2}:\d{2}:\d{2}
一个无符号整数
\d+
一个可能为空的空格序列
\s*
一个无符号整数,其前后可能有空格
\s*\d+\s*
一个可能带符号的整数,其前后可能有空格
\s*[+|-]?\s*\d+\s*
一个无符号实数,其前后可能有空格
\s*\d+(.\d*)?\s*
一个可能带符号且前后带有空格的实数
\s*[+|]?\s*\d+(.\d*)?\s*
包含字符串 juste 的字符串
\bjuste\b
  

可以指定在字符串中搜索模式的位置:

模式
含义
^模式
模式位于字符串开头
模式$
模式结束字符串
^模式$
模式开头和结尾
模式
从字符串开头开始,在整个字符串中搜索该模式。
要搜索的字符串
模式
以感叹号结尾的字符串
!$
以句点结尾的字符串
\.$
以 // 序列开头的字符串
^//
仅包含一个单词(前后可能有空格)的字符串
^\s*\w+\s*$
一个字符串,包含两个单词,前后可能有空格
^\s*\w+\s*\w+\s*$
包含单词 secret 的字符串
\bsecret\b

模式的子集可以被“提取”。因此,我们不仅可以验证一个字符串是否符合特定模式,还可以从该字符串中提取那些用圆括号括起的、与模式子集对应的元素。 因此,如果要分析包含日期格式 dd/mm/yy 的字符串,并希望从中提取日期中的 dd、mm、yy 这三个元素,则应使用正则表达式 (\d\d)/(\d\d)/(\d\d)。

5.7.1. 验证字符串是否符合给定模式

Regex 类型的对象构建方式如下:

public Regex(string pattern)
construit un objet "expression régulière" à partir d'un modèle passé
en paramètre (pattern)

构建好正则表达式模板后,可以使用 IsMatch 方法将其与字符串进行比较:

public bool IsMatch(string input)
vrai si la chaîne input correspond au modèle de l'expression
régulière

以下是一个示例:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // 正则表达式模式
            string modèle1 = @"^\s*\d+\s*$";
            Regex regex1 = new Regex(modèle1);
            // 将实例与模式进行比较
            string exemplaire1 = "  123  ";
            if (regex1.IsMatch(exemplaire1)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire1, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire1, modèle1);
            }//if
            string exemplaire2 = "  123a  ";
            if (regex1.IsMatch(exemplaire2)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire2, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire2, modèle1);
            }//if
        }

    }
}

以及执行结果:

[  123  ] correspond au modèle [^\s*\d+\s*$]
[  123a  ] ne correspond pas au modèle [^\s*\d+\s*$]

5.7.2. 在字符串中查找某模式的所有出现位置

Matches 方法可用于检索字符串中与模式匹配的元素:

public MatchCollection Matches(string input)
rend la collection des éléments de la chaîne input
correspondant au modèle

MatchCollection 具有一个 Count 属性,该属性表示集合中的元素数量。 如果 résultats 是一个 MatchCollection 对象,则 results[i] 是该集合中的第 i 个元素,且类型为 MatchMatch 类具有多种属性,包括以下内容:

  • Value:对象 Match, 的值,即与该模型匹配的元素
  • Index:该元素在遍历的字符串中被找到的位置

让我们来看以下示例:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // 正文中包含多个模式匹配项
            string modèle2 = @"\d+";
            Regex regex2 = new Regex(modèle2);
            string exemplaire3 = "  123  456  789 ";
            MatchCollection résultats = regex2.Matches(exemplaire3);
            Console.WriteLine("Modèle=[{0}],exemplaire=[{1}]", modèle2, exemplaire3);
            Console.WriteLine("Il y a {0} occurrences du modèle dans l'exemplaire ", résultats.Count);
            for (int i = 0; i < résultats.Count; i++) {
                Console.WriteLine("[{0}] trouvé en position {1}", résultats[i].Value, résultats[i].Index);
            }//for
        }
    }
}
  • 第 8 行:要查找的模式是一串数字
  • 第 10 行:包含该模式的字符串
  • 第 11 行:提取 exemplaire3 中所有符合模式 modèle2 的元素
  • 第14-16行:显示这些元素

程序执行结果如下:

1
2
3
4
5
Modèle=[\d+],exemplaire=[  123  456  789 ]
Il y a 3 occurrences du modèle dans l'exemplaire
[123] trouvé en position 2
[456] trouvé en position 7
[789] trouvé en position 12

5.7.3. 提取模板的子集

可以“提取”模式的子集。因此,我们不仅可以验证字符串是否符合特定模式,还可以从该字符串中提取模式中用圆括号括起的子集对应的元素。 例如,若要分析包含日期格式 jj/mm/aa 的字符串,并提取其中的 jj、mm、aa 元素,可使用模式 (\d\d)/(\d\d)/(\d\d)。

让我们来看以下示例:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program3 {
        static void Main(string[] args) {
            // 从模板中提取元素
            string modèle3 = @"(\d\d):(\d\d):(\d\d)";
            Regex regex3 = new Regex(modèle3);
            string exemplaire4 = "Il est 18:05:49";
            // 模板验证
            Match résultat = regex3.Match(exemplaire4);
            if (résultat.Success) {
                // 实例与模板匹配
                Console.WriteLine("L'exemplaire [{0}] correspond au modèle [{1}]",exemplaire4,modèle3);
                // 显示括号组
                for (int i = 0; i < résultat.Groups.Count; i++) {
                    Console.WriteLine("groupes[{0}]=[{1}] trouvé en position {2}",i, résultat.Groups[i].Value,résultat.Groups[i].Index);
                }//for
            } else {
                // 实例与模板不匹配
                Console.WriteLine("L'exemplaire[{0}] ne correspond pas au modèle [{1}]", exemplaire4, modèle3);
            }
        }
    }
}

执行该程序将产生以下结果:

1
2
3
4
5
L'exemplaire [Il est 18:05:49] correspond au modèle [(\d\d):(\d\d):(\d\d)]
groupes[0]=[18:05:49] trouvé en position 7
groupes[1]=[18] trouvé en position 7
groupes[2]=[05] trouvé en position 10
groupes[3]=[49] trouvé en position 13

新内容位于第 12-19 行:

  • 第 12 行:字符串 exemplaire4 通过方法 Match 与模板 regex3 进行比较。该方法返回了一个此前已介绍过的 Match 对象。 此处我们使用了该类的两个新属性:
  • Success(第13行):指示是否匹配成功
  • Groups(第 17、18 行):集合,其中
    • Groups[0] 对应于字符串中与模板
    • Groups[i](i>=1)对应第 i 号括号组

如果 résultat 的类型为 Match, 则 résultats.Groups 的类型为 GroupCollection,而 résultats.Groups[i] 的类型为 GroupGroup 类有两个属性,我们在此使用的是:

  • Value(第 18 行):对象 Group 的值,该对象是括号内容对应的元素
  • Index(第18行):该元素在遍历字符串中被找到的位置

5.7.4. 一个练习程序

寻找能够验证字符串是否符合特定模式的正则表达式,有时是一项真正的挑战。以下程序可用于练习。它需要一个模式和一个字符串,并指出该字符串是否符合该模式。


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program4 {
        static void Main(string[] args) {
            // 数据
            string modèle, chaine;
            Regex regex = null;
            MatchCollection résultats;
            // 系统要求用户指定与该实例进行比较的模板和实例
            while (true) {
                // 要求提供模板
                Console.Write("Tapez le modèle à tester ou rien pour arrêter :");
                modèle = Console.In.ReadLine();
                // 完成了吗?
                if (modèle.Trim() == "")
                    break;
                // 创建正则表达式
                try {
                    regex = new Regex(modèle);
                } catch (Exception ex) {
                    Console.WriteLine("Erreur : " + ex.Message);
                    continue;
                }
                // 询问用户需要与模板进行比对的副本
                while (true) {
                    Console.Write("Tapez la chaîne à comparer au modèle [{0}] ou rien pour arrêter :", modèle);
                    chaine = Console.ReadLine();
                    // 完成?
                    if (chaine.Trim() == "")
                        break;
                    // 进行比较
                    résultats = regex.Matches(chaine);
                    // 成功了吗?
                    if (résultats.Count == 0) {
                        Console.WriteLine("Je n'ai pas trouvé de correspondances");
                        continue;
                    }//if
                    // 显示与模板匹配的元素
                    for (int i = 0; i < résultats.Count; i++) {
                        Console.WriteLine("J'ai trouvé la correspondance [{0}] en position [{1}]", résultats[i].Value, résultats[i].Index);
                        // 子元素
                        if (résultats[i].Groups.Count != 1) {
                            for (int j = 1; j < résultats[i].Groups.Count; j++) {
                                Console.WriteLine("\tsous-élément [{0}] en position [{1}]", résultats[i].Groups[j].Value, résultats[i].Groups[j].Index);
                            }
                        }
                    }
                }
            }
        }
    }
}

以下是一个运行示例:

Tapez le modèle à tester ou rien pour arrêter :\d+
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :123 456 789
J'ai trouvé la correspondance [123] en position [0]
J'ai trouvé la correspondance [456] en position [4]
J'ai trouvé la correspondance [789] en position [8]
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :(\d{2}):(\d\d)
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :14:15 abcd 17:18 xyzt
J'ai trouvé la correspondance [14:15] en position [0]
        sous-élément [14] en position [0]
        sous-élément [15] en position [3]
J'ai trouvé la correspondance [17:18] en position [11]
        sous-élément [17] en position [11]
        sous-élément [18] en position [14]
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*\d+\s*$
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :   1456
J'ai trouvé la correspondance [   1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*(\d+)\s*$
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :1456
J'ai trouvé la correspondance [1456] en position [0]
        sous-élément [1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :abcd 1456
Je n'ai pas trouvé de correspondances
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :

5.7.5. Split方法

我们在课程 String 中已经接触过这种方法:


public string[] Split(char[] separator)
la chaîne est vue comme une suite de champs séparés par les
caractères présents dans le tableau separator. Le résultat est
le tableau de ces champs

Regex 类中的 Split 方法允许我们根据模板来指定分隔符:


public string[] Split(string input)
La chaîne input est décomposée en champs, ceux-ci étant séparés
par un séparateur correspondant au modèle de l'objet Regex
courant.

例如,假设某个文本文件中包含形式为字段1、字段2、……、字段n的行。字段之间用逗号分隔,但逗号前后可能有空格。 此时,类 string 中的方法 Split 并不适用。方法 RegEx 则提供了解决方案。如果 ligne 是读取的行,则可通过以下方式获取字段:

string[] champs=new Regex(@"s*,\s*").Split(ligne);

获取字段,如下例所示:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program5 {
        static void Main(string[] args) {
            // 一行
            string ligne = "abc  , def  , ghi";
            // 一个模板
            Regex modèle = new Regex(@"\s*,\s*");
            // 将行拆分为字段
            string[] champs = modèle.Split(ligne);
            // 显示
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("champs[{0}]=[{1}]", i, champs[i]);
            }
        }
    }
}

执行结果:

1
2
3
champs[0]=[abc]
champs[1]=[def]
champs[2]=[ghi]

5.8. 示例应用程序 - V3

我们继续探讨第 3.6 节(版本 1)和第 4.10 节(版本 2)中分析过的应用程序。

在研究的最新版本中,税额计算是在抽象类 AbstractImpot 中进行的:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {

        // 计算税款所需的税率区间
        // 来自外部来源

        protected TrancheImpot[] tranchesImpot;

        // 税额计算
        public int calculer(bool marié, int nbEnfants, int salaire) {
            // 份额数量计算
            decimal nbParts;
            if (marié) 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;
            // 税额计算
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
            // 返回结果
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//计算
    }//分类

}

第 38 行中的方法 calculer 使用了第 35 行中的数组 tranchesImpot,而该数组并未由类 AbstractImpot 进行初始化。 因此该方法为抽象方法,必须通过派生类实现才能生效。该初始化操作由派生类 HardwiredImpot 完成:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

        // 计算税额所需的数据表
        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 };

        public HardwiredImpot() {
                // 创建税率表
            tranchesImpot = new TrancheImpot[limites.Length];
                // 填充
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
    }// 类
}// 命名空间

在上文中,计算税款所需的数据被“硬编码”在类代码中。示例的新版本将其放置在文本文件中:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

由于处理此文件可能会引发异常,因此我们创建了一个专门的类来处理这些异常:


using System;

namespace Chap3 {
    class FileImpotException : Exception {
        // 错误代码
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
    
        // 错误代码
        public CodeErreurs Code { get; set; }

        // 构造函数
        public FileImpotException() {
        }
        public FileImpotException(string message)
            : base(message) {
        }
        public FileImpotException(string message, Exception e)
            : base(message,e) {
        }
    }
}
  • 第 4 行:类 FileImpotException 继承自类 Exception。它将用于记录在处理数据文本文件时发生的任何错误。
  • 第 7 行:一个表示错误代码的枚举:
    • Acces:访问数据文本文件时发生错误
    • Ligne:行中缺少预期的三个字段
    • Champ1:第 1 字段有误
    • Champ2:第 2 个字段有误
    • Champ3:字段 3 错误

其中某些错误可能同时出现(Champ1Champ2Champ3)。 因此,枚举 CodeErreurs 被标注了属性 [Flags],这意味着该枚举的各个值必须是 2 的幂。 因此,第 1 和第 2 字段的错误将导致错误代码 Champ1 | Champ2

  • 第 10 行:自动属性 Code 将存储错误代码。
  • 第15行:一个构造函数,用于通过传递错误消息作为参数来创建 FileImpotException 对象。
  • 第18行:一个构造函数,用于通过传入错误消息和引发该错误的异常作为参数来创建一个FileImpotException对象。

初始化类 AbstractImpot 中数组 tranchesImpot 的类现为:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace Chap3 {
    class FileImpot : AbstractImpot {

        public FileImpot(string fileName) {
            // 数据
            List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
            int numLigne = 1;
            // 异常
            FileImpotException fe = null;
            // 逐行读取文件 fileName 的内容
            Regex pattern = new Regex(@"s*:\s*");
            // 起初没有错误
            FileImpotException.CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(fileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // 当前行
                        string ligne = input.ReadLine().Trim();
                        // 忽略空行
                        if (ligne == "") continue;
                        // 行被拆分为三个字段,由以下符号分隔:
                        string[] champsLigne = pattern.Split(ligne);
                        // 是否有 3 个字段?
                        if (champsLigne.Length != 3) {
                            code = FileImpotException.CodeErreurs.Ligne;
                        }
                        // 3个字段的转换
                        decimal limite = 0, coeffR = 0, coeffN = 0;
                        if (code == 0) {
                            if (!Decimal.TryParse(champsLigne[0], out limite)) code = FileImpotException.CodeErreurs.Champ1;
                            if (!Decimal.TryParse(champsLigne[1], out coeffR)) code |= FileImpotException.CodeErreurs.Champ2;
                            if (!Decimal.TryParse(champsLigne[2], out coeffN)) code |= FileImpotException.CodeErreurs.Champ3; ;
                        }
                        // 错误?
                        if (code != 0) {
                            // 记录错误
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // 记录新的税率区间
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // 下一行
                            numLigne++;
                        }
                    }
                }
                // 将列表 listImpot 传输到数组 tranchesImpot
                if (code == 0) {
                    // 将列表 listImpot 转移到数组 tranchesImpot
                    tranchesImpot = listTranchesImpot.ToArray();
                }
            } catch (Exception e) {
                // 记录错误
                fe= new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", fileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // 需要报告错误吗?
            if (fe != null) throw fe;
        }
    }
}
  • 第 7 行:类 FileImpot 继承自类 AbstractImpot,这与版本 2 中类 HardwiredImpot 的继承关系相同。
  • 第 9 行:类 FileImpot 的构造函数用于初始化其基类 AbstractImpot 的字段 trancheImpot。该构造函数接受一个参数,即包含数据的文本文件名。
  • 第11行:基类AbstractImpot的字段tranchesImpot是一个数组,需使用作为参数传递的文件filename中的数据进行填充。文本文件的读取是顺序进行的。 只有在读取完整个文件后,才能知道行数。因此无法预先确定数组 tranchesImpot 的大小我们将暂时将数据存储在通用列表 listTranchesImpot

需要提醒的是,类型 TrancheImpot 是一个结构体:


namespace Chap3 {
    // 一个税级
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}
  • 第 14 行:类型为 FileImpotExceptionfe 用于封装文本文件处理过程中可能出现的错误。
  • 第16行:文本文件中champ1:champ2:champ3行中字段分隔符的正则表达式。字段由字符:分隔,该字符前后可跟随任意数量的空格。
  • 第 18 行:发生错误时的错误代码
  • 第 20 行:使用 StreamReader 处理文本文件
  • 第 21 行:只要还有待读取的行且未发生错误,就循环处理
  • 第 27 行:利用第 16 行的正则表达式将读取的行拆分为字段
  • 第29-31行:验证该行是否确实包含三个字段——记录可能出现的错误
  • 第33-38行:将三个字符串转换为三个十进制数——记录可能出现的错误
  • 第40-43行:若出现错误,则抛出类型为FileImpotException的异常。
  • 第44-47行:若无错误,在保存当前行数据后,继续读取文本文件的下一行。
  • 第 52-55 行: 当 while 输出时,通用列表 listTranchesImpot 中的数据会被复制到基类 AbstractImpot 的数组 tranchesImpot 中。 需要指出的是,这正是构造函数的设计目的。
  • 第 56-59 行:处理可能发生的异常。该异常被封装在一个类型为 FileImpotException 的对象中。
  • 第 61 行:如果第 18 行中的 fe 异常已被初始化,则触发该异常。

整个 C# 项目如下:

  • 在 [1] 中:整个项目
  • 在 [2,3] 中:[DataImpot.txt] 和 [2] 文件的属性。[Copy to Output Directory] 和 [3] 的属性被设置为 always。 这意味着每次运行时,文件 [DataImpot.txt] 都会被复制到文件夹 bin/Release(Release 模式)或 bin/Debug(Debug 模式)中。可执行文件会在此处查找该文件。
  • 在 [4] 中:对文件 [DataImpotInvalide.txt] 也进行同样的操作。

[DataImpot.txt] 的内容如下:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

[DataImpotInvalide.txt] 的内容如下:

a:b:c

测试程序 [Program.cs] 未作更改:其内容与第 2 版第 4.10 节中的内容一致,仅有一处差异:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
...
             // 创建对象 IImpot
            IImpot impot = null;
            try {
                // 创建对象 IImpot
                impot = new FileImpot("DataImpot.txt");
            } catch (FileImpotException e) {
                // 显示错误
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // 程序停止
                Environment.Exit(1);
            }

            // 无限循环
            while (true) {
...
            }//while
        }
    }
}
  • 第 8 行:类型为 IImpot 接口的 impot 对象
  • 第 11 行:使用类型为 FileImpot 的对象实例化 impot 对象。此操作可能会引发异常,该异常由第 9、12 和 18 行的 try/catch 语句进行处理。

以下是执行示例:

使用文件 [DataImpot.txt]

1
2
3
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :

使用不存在的文件 [xx]

L'erreur suivante s'est produite : [Code=Acces,Message=Erreur lors de la lecture du fichier xx, Exception d'origine : Could not find file 'C:\data\2007-2008\c#2008\poly\Chap3\10\bin\Release\xx'.]

使用文件 [DataImpotInvalide.txt]

L'erreur suivante s'est produite : [Code=Champ1, Champ2, Champ3,Message=Ligne n° 1 incorrecte]