Attribute特性简介

Attribute 特性

特性简介

特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。您可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。

特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。

与注释的区别: 注释仅在 IDE 环境中使用,特性可以影响编译器或程序运行,可以在不破坏类型封装的前提下,为对象增加额外的信息,执行额外的行为。

特性语法

1
2
[attribute(positional_parameters, name_parameter = value, ...)]  
element

特性(Attribute)的名称和值是在方括号内规定的,放置在它所应用的元素之前。positional_parameters 规定必需的信息,name_parameter 规定可选的信息。

预定义特性

AttributeUsage

预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。

1
[AttributeUsage(validon, AllowMultiple=allowmultiple, Inherited=inherited)]
  • 参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All
  • 参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。
  • 参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。

例如:

1
[AttributeUsage(AttributeTargets.Class | ttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)]

Conditional

这个预定义特性标记了一个条件方法,其执行依赖于它顶的预处理标识符。它会引起方法调用的条件编译,取决于指定的值,比如 DebugTrace。例如,当调试代码时显示变量的值。

1
[Conditional(conditionalSymbol)]

例如:

1
[Conditional("DEBUG")]

注意: Conditional 特性的方式与 #if...#endifConditional 特性的方法是否生效是取决于调用方,而用 #if 方式是否生效是取决于方法定义所在的程序集。

1
2
3
4
5
6
7
8
9
10
11
12
13
[Conditional("Conditional")]
private void TestOne()
{
//需要设置项目 属性 -> 生成 -> 条件编译符号 为:Conditional
Console.WriteLine("this is test function one.");
}
private void TestTwo()
{
//需要设置项目 属性 -> 生成 -> 条件编译符号 为:Conditional
#if Conditional
Console.WriteLine("this is test function two.");
#endif
}

注意:Conditional 特性可以多个,同时使用多个多个编译符号,多个之间是“或”的关系。

Obsolete

这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。

规定该特性的语法如下:

[Obsolete(message)][Obsolete(message, iserror)]

  • 参数 message,是一个字符串,描述项目为什么过时的原因以及该替代使用什么。
  • 参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。

自定义特性

.Net 框架允许创建自定义特性,用于存储声明性的信息,且可在运行时被检索。该信息根据设计标准和应用程序需要,可与任何目标元素相关。

创建并使用自定义特性包含四个步骤:

  • 声明自定义特性;构建自定义特性;
  • 在目标程序元素上应用自定义特性;
  • 通过反射访问特性;
  • 最后一个步骤包含编写一个简单的程序来读取元数据以便查找各种符号。元数据是用于描述其他数据的数据和信息。该程序应使用反射来在运行时访问特性。

声明自定义特性

一个新的自定义特性应派生自 System.Attribute 类。例如:

1
2
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)]
public class DeveloperInfoAttribute : Attribute { }

构建自定义特性

每个特性必须至少有一个构造函数。必需的定位(positional)参数应通过构造函数传递。例如:

1
2
3
4
5
6
7
8
9
10
11
12
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)]
public class DeveloperInfoAttribute : Attribute
{
public DeveloperInfoAttribute(string name, string date)
{
DeveloperName = name;
DeveloperDate = date;
}
public string DeveloperName { get; set; }
public string DeveloperDate { get; set; }
public string Describe { get; set; }
}

应用自定义特性

通过把特性放置在紧接着它的目标之前,来应用该特性。例如:

1
2
3
4
5
6
7
8
[DeveloperInfo("John", "2018-06-23", Describe = "this is a test class.")]
public class Business
{
[DeveloperInfo("John", "2018-06-23", Describe = "this is a test property.")]
public int TestProperty { get; set; }
[DeveloperInfo("John", "2018-06-23", Describe = "this is a test method.")]
public void TestMethod() { }
}

通过反射访问特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
object[] attr = typeof(Business).GetCustomAttributes(false);
for (int i = 0; i < attr.Length; i++)
{
if (attr[i] is DeveloperInfoAttribute)
{
DeveloperInfoAttribute developer = attr[i] as DeveloperInfoAttribute;
Console.WriteLine($"Name:{developer.DeveloperName} Date:{developer.DeveloperDate} Describe:{developer.Describe}");
}
}
}
{
MethodInfo[] method = typeof(Business).GetMethods();
for (int i = 0; i < method.Length; i++)
{
object[] attr = method[i].GetCustomAttributes(false);
for (int j = 0; j < attr.Length; j++)
{
if (attr[j] is DeveloperInfoAttribute)
{
DeveloperInfoAttribute developer = attr[j] as DeveloperInfoAttribute;
Console.WriteLine($"Name:{developer.DeveloperName} Date:{developer.DeveloperDate} Describe:{developer.Describe}");
}
}
}
}
{
PropertyInfo[] property = typeof(Business).GetProperties();
for (int i = 0; i < property.Length; i++)
{
object[] attr = property[i].GetCustomAttributes(false);
for (int j = 0; j < attr.Length; j++)
{
if (attr[j] is DeveloperInfoAttribute)
{
DeveloperInfoAttribute developer = attr[j] as DeveloperInfoAttribute;
Console.WriteLine($"Name:{developer.DeveloperName} Date:{developer.DeveloperDate} Describe:{developer.Describe}");
}
}
}
}

注意事项

  • 自定义的 Attribute 必须直接或者间接继承 System.Attribute
  • 这里有一个约定:所有自定义的特性名称都应该有个 Attribute 后缀。因为当你的 Attribute 施加到一个程序的元素上的时候,编译器先查找你的 Attribute 的定义,如果没有找到,那么它就会查找 特性名称 + Attribute 的定义。如果都没有找到,那么编译器就报错。
  • Attribute 可以关联的元素包括:程序集(assembly)、模块(module)、类型(type)、属性(property)、事件(event)、字段(field)、方法(method)、参数(param)、返回值(return)。
  • AttributeTargets 目标包括:
    • All:可以对任何应用程序元素应用属性;
    • Assembly:可以对程序集应用属性;
    • Class:可以对类应用属性;
    • Constructor:可以对构造函数应用属性;
    • Delegate:可以对委托应用属性;
    • Enum:可以对枚举应用属性;
    • Event:可以对事件应用属性;
    • Field:可以对字段应用属性;
    • GenericParameter:可以对泛型参数应用属性;
    • Interface:可以对接口应用属性;
    • Method:可以对方法应用属性;
    • ModuleModule 指的是可移植的可执行文件(.dll.exe),而非 Visual Basic 标准模块;
    • Parameter:可以对参数应用属性;
    • Property:可以对属性 (Property) 应用属性 (Attribute);
    • ReturnValue:可以对返回值应用属性;
    • Struct:可以对结构应用属性,即值类型。
  • AttributeUsageAttribute中的3个属性(Property)说明:
    • ValidOn:该定位参数指定可在其上放置所指示的属性 (Attribute) 的程序元素。AttributeTargets 枚举数中列出了可在其上放置属性 (Attribute) 的所有可能元素的集合。可通过按位“或”运算组合多个 AttributeTargets 值,以获取所需的有效程序元素组合。
    • AllowMultiple:该命名参数指定能否为给定的程序元素多次指定所指示的属性。
    • Inherited:该命名参数指定所指示的属性能否由派生类和重写成员继承。
  • Attribute 检测方法:
    • IsDefined:如果至少有一个指定的 Attribute 派生类的实例与目标关联,就返回 true。这个方法效率很高,因为他不构造(反序列化)Attribute 类的任何实例。
    • GetCustomAttributes:返回一个数组,其中每个元素都是应用于目标的指定 Attribute 类的一个实例。
    • GetCustomAttributesData:获取 CustomAttributesData 特性信息。

正则表达式总结

正则表达式

正则表达式作为一个强大的字符串处理方案,一直受到广大程序员的青睐。其不但使用灵活,而且字符串处理的速度也基本被各种编程语言优化到极致,在字符串匹配、提取、替换、分割等一直是首选的方案。

最近有一个前端妹子突然问了一个非常简单的正则表达式应该怎么写,我诧异于她不会写正则表达式的同时,思考了一下好像身边同事,真会使用正则表达式的也没有几个。

恰好最近在读《JavaScript面向对象编程指南》这本书的RegExp(JavaScript中的正则表达式对象)这一章,那就趁此机会总结一下平时工作学习中使用正则表达式的场景,同时整理一下这部分的基本知识。

本文代码均使用 C# 编写,后面有空会针对不同的编程语言,介绍各自的正则表达式对象怎么使用。

校验

正则我们使用最多就是校验了,也就是对一个字符串进行匹配,确认一个字符串是否符合要求,例如判断一个IP是否合法、用户名是否符合系统要求、邮箱输入合法、输入内容是否是数字等。

从用户名校验开始

经常能看到一些网站或系统中,设计用户名规则为:“只能使用数字或英文,并且首字母必须是字母,长度限制在6-32以内”。

如果没有正则表达式,代码的实现我们可能需要这么设计:循环字符串中每一个字符,判断其是否符合限制要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// <summary>
/// 验证用户名是否合法:6-32位字母数字组成,首字母非数字
/// </summary>
/// <param name="name">用户名</param>
/// <returns>用户名是否符合系统设计规则要求</returns>
public static bool CheckUserName(string name)
{
//验证长度限制
if (name.Length < 6 || name.Length > 32)
return false;

for (int i = 0; i < name.Length; i++)
{
char ch = name[i];

//必须是数字或大小写字母
if (ch < 48 || ch > 57 && ch < 65 || ch > 95 && ch < 97)
return false;

//首字母不能是数字
if (i == 0 && ch >= 48 && ch <= 57)
return false;
}
return true;
}

以上已经尽量优化,用字符对应的编码字符来判断是否在数字或大小写字母的编码区间内,小明同学看了表示这个已经不错了,但是如果我们用正则表示呢?

1
2
3
4
5
6
7
8
9
/// <summary>
/// 验证用户名是否合法:6-32位字母数字组成,首字母非数字
/// </summary>
/// <param name="name">用户名</param>
/// <returns>用户名是否符合系统设计规则要求</returns>
public static bool CheckUserName(string name)
{
return System.Text.RegularExpressions.Regex.IsMatch(name, "^[a-zA-Z][a-zA-Z0-9]{5,31}$");
}

只需要一行代码,就可以实现上面对于用户名的校验,当然这还只是简单的用户名校验问题,如果是针对更复杂的例如IP地址/身份证/手机号码/邮箱/网址等,正则的优势还会更明显,因为这些内容我们同样可以使用一个正则表达式来实现匹配校验。

常用的正则表达式

转自:最全的常用正则表达式大全——包括校验数字、字符、一些特殊的需求等等

校验数字的表达式

  1. 数字:^[0-9]*$
  2. n位的数字:^\d{n}$
  3. 至少n位的数字:^\d{n,}$
  4. m-n位的数字:^\d{m,n}$
  5. 零和非零开头的数字:^(0|[1-9][0-9]*)$
  6. 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
  7. 带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
  8. 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
  9. 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
  10. 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
  11. 非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
  12. 非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
  13. 非负整数:^\d+$ 或 ^[1-9]\d*|0$
  14. 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
  15. 非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
  16. 非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
  17. 正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
  18. 负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
  19. 浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$

校验字符的表达式

  1. 汉字:^[\u4e00-\u9fa5]{0,}$
  2. 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
  3. 长度为3-20的所有字符:^.{3,20}$
  4. 由26个英文字母组成的字符串:^[A-Za-z]+$
  5. 由26个大写英文字母组成的字符串:^[A-Z]+$
  6. 由26个小写英文字母组成的字符串:^[a-z]+$
  7. 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
  8. 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
  9. 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
  10. 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
  11. 可以输入含有^%&’,;=?$"等字符:[^%&',;=?$\x22]+
  12. 禁止输入含有的字符:`[^\x22]+`

特殊需求表达式

  1. Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
  2. 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
  3. InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
  4. 手机号码:^(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])\d{8}$ (由于工信部放号段不定时,所以建议使用泛解析 ^([1][3,4,5,6,7,8,9])\d{9}$)
  5. 电话号码(“XXX-XXXXXXX”、”XXXX-XXXXXXXX”、”XXX-XXXXXXX”、”XXX-XXXXXXXX”、”XXXXXXX”和”XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$
  6. 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
  7. 18位身份证号码(数字、字母x结尾):^((\d{18})|([0-9x]{18})|([0-9X]{18}))$
  8. 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
  9. 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
  10. 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
  11. 日期格式:^\d{4}-\d{1,2}-\d{1,2}
  12. 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
  13. 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
  14. 钱的输入格式:
  15. 有四种钱的表示形式我们可以接受:”10000.00” 和 “10,000.00”, 和没有 “分” 的 “10000” 和 “10,000”:^[1-9][0-9]*$
  16. 这表示任意一个不以0开头的数字,但是,这也意味着一个字符”0”不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$
  17. 一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
  18. 这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
  19. 必须说明的是,小数点后面至少应该有1位数,所以”10.”是不通过的,但是 “10” 和 “10.2” 是通过的:^[0-9]+(.[0-9]{2})?$
  20. 这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
  21. 这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
  22. 1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
  23. xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
  24. 中文字符的正则表达式:[\u4e00-\u9fa5]
  25. 双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
  26. 空白行的正则表达式:\n\s*\r (可以用来删除空白行)
  27. HTML标记的正则表达式:<(\S*?)[^>]*>.*?</\1>|<.*? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
  28. 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
  29. 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
  30. 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
  31. IP地址:((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))

以上是一些网上流传的正则表达式,这些内容仅供参考,如果想要确认以上正则表达式是否符合自己的使用场景,或者自己实现一个正则表达式,请看最后一个部分。

提取

提取顾名思义也就是从一段字符串,获取我们需要的特定内容,例如在网页爬虫中,我们常常需要获取html中的文本内容,或特定标签下的内容。(当然我们可以使用XPath语法进行解析,但是正则也不失为一个优秀的解决方案,而且很多XPath方案的底层是基于正则实现的。后面介绍爬虫的时候,再具体介绍XPath。)

如果内容是我们需要提取的内容,我们可以使用( )将需要提取的内容包起来,这样正则匹配时会将这部分内容提取到集合中,我们可以从集合中获取这些元素。

提取邮箱中的用户名

C# 中,提取到的内容在Group集合中,集合中第一个元素为匹配的所有内容,例如下面代码第一个元素是匹配到的邮箱,而后的元素才是我们使用小括号提取的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 验证邮箱并提取邮箱中的用户名
/// </summary>
/// <param name="email">邮箱</param>
/// <param name="name">用户名</param>
/// <returns>邮箱内容是否合法</returns>
public static bool CheckEmail(string email, out string name)
{
name = null;
System.Text.RegularExpressions.Match match
= System.Text.RegularExpressions.Regex.Match(email, @"^(\w+([-+.]\w+)*)@\w+([-.]\w+)*\.\w+([-.]\w+)*$");

if (match.Success)
{
name = match.Groups[1].Value;
}

return match.Success;
}

简单测试:

1
2
3
4
5
string email = "hd2y@outlook.com";
if (CheckEmail(email, out string name))
{
Console.WriteLine($"用户{name}邮箱验证合法:{email}");
}

提取Html内容中所有标题

在邮箱匹配的例子中我们对内容进行了限定,^代表匹配开头,$代表匹配结尾,所以其实际上是对整个内容进行匹配。

但是我们可能会碰到对内容进行多次匹配的情况,例如提取html内容中所有的标题也就是h1-h6标签,这就是一个循环匹配的过程,所以我们不能限定开始于结束。

另外一个标签有标签名、属性、内容,当让我们可以使用上文的方式,在匹配到的内容通过索引去获取内容,但是正则也允许我们使用别名,所以以下的例子中,我们将为其取别名,然后通过别名来获取内容。同时我们还可以使用别名配合\k(命名向后引用)来确定一个标签的闭合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static IEnumerable<Title> FetchTitle(string title)
{
foreach (System.Text.RegularExpressions.Match item
in System.Text.RegularExpressions.Regex.Matches(title, @"<(?'TN'H[1-6])\s*(?'TP'[^<>]*)>(?'TC'.*?)</\k'TN'>"
, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
yield return new Title()
{
Name = item.Groups["TN"].Value,
Property = item.Groups["TP"].Value,
Content = item.Groups["TC"].Value
};
}
}

public class Title
{
public string Name { get; set; }
public string Property { get; set; }
public string Content { get; set; }
}

简单测试:

1
2
3
4
5
string html = "<h1>h1</h1><h2>h2</h2><h3>h3</h3><h4>h4</h4><h5 style=\"color:red;\">h5</h5><h6>h6</h6>";
foreach (Title title in FetchTitle(html))
{
Console.WriteLine($"获取到标题{title.Name} 内容:{title.Content} 属性:{title.Property}");
}

以上匹配部分写法并不严格,例如标题标签嵌套其他标签内容时可能存在问题,这里只是简单举例,不考虑复杂正则表达式写法。

Html内容提取可以参考 Aiursoft.HSharp 项目,里面对于Html的解析较为标准可用。

分割

对于字符串操作,大部分编程语言都提供了Split方法,同样的正则也有类似的方法,但是正则分割适用范围更广,适合在分隔符不确定但是有一定规律的情况下使用。

分割时间信息

1
string[] dateInfo = Regex.Split("2019-04-01 00:00:00", @"\W");

提取Pascal方法名中的单词

1
string[] words = Regex.Split("ILoveYou", "(?!^)(?=[A-Z])");

(?!...) 正向否定预查;(?=...) 正向肯定预查。

替换

在我的日常工作中,正则使用频率一般是 匹配 > 提取 > 替换 > 分割,替换在一些特定环境下,真的是让人有种相见恨晚的感觉。

处理ASTM协议消息

在工作中有时会和其他系统和设备打交道,做一些接口,他们都有自己定义的通讯协议,而ASTM就是其中一种。

刚开始处理这个协议的一个问题就在于,这个协议会将完整的消息分块,然后加入校验字符,类似下面这种结果:(以下内容仅仅是简单举例,验证内容未经实际计算,ASTM协议更多解释可以搜索相关文档了解)

1
2
3
<STX>1AAA<ETB>1F<CR><LF>
<STX>2BBB<ETB>2E<CR><LF>
<STX>3CCC<ETX>3D<CR><LF>

其中我们需要的只是其中的AAABBBCCC,但是其为了确认消息的完整性加入了一些消息块标识和校验码,这些不是我们需要的但是使用字符串自带的Replace或者Split取消息部分就比较麻烦,至少需要写一个循环。

而正则就相对简单的多:

1
2
string content = "";//内容略
content = Regex.Replace(content, @"(\u0002[0-7]{1})|((\u0003|\u0017)[0-9A-F]{2}\r\n)", "");

删除重复单词

1
2
string content = "I love you very very very much.";//内容略
content = Regex.Replace(content, @"(?<word>[a-zA-Z]+)( \k<word>)+", "${word}");

以上只是一些使用场景的简单举例,以下部分时摘抄自菜鸟教程关于正则表达式部分的文档,供学习或者查阅时使用。

语法

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

例如:

  • runoo+b,可以匹配 runoob、runooob、runoooooob 等,+ 号代表前面的字符必须至少出现一次(1次或多次)。
  • runoo*b,可以匹配 runob、runoob、runoooooob 等,* 号代表字符可以不出现,也可以出现一次或者多次(0次、或1次、或多次)。
  • colou?r 可以匹配 color 或者 colour,? 问号代表前面的字符最多只可以出现一次(0次、或1次)。

构造正则表达式的方法和创建数学表达式的方法一样。也就是用多种元字符与运算符可以将小的表达式结合在一起来创建更大的表达式。正则表达式的组件可以是单个的字符、字符集合、字符范围、字符间的选择或者所有这些组件的任意组合。

正则表达式是由普通字符(例如字符 a 到 z)以及特殊字符(称为”元字符”)组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

普通字符

普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

非打印字符

非打印字符也可以是正则表达式的组成部分。下表列出了表示非打印字符的转义序列:

字符 描述
\cx 匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\f 匹配一个换页符。等价于 \x0c 和 \cL。
\n 匹配一个换行符。等价于 \x0a 和 \cJ。
\r 匹配一个回车符。等价于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]
\t 匹配一个制表符。等价于 \x09 和 \cI。
\v 匹配一个垂直制表符。等价于 \x0b 和 \cK。

特殊字符

所谓特殊字符,就是一些有特殊含义的字符,如上面说的 runoo*b 中的 *,简单的说就是表示任何字符串的意思。如果要查找字符串中的 * 符号,则需要对 * 进行转义,即在其前加一个 \: runo\*ob 匹配 runo*ob

许多元字符要求在试图匹配它们时特别对待。若要匹配这些特殊字符,必须首先使字符”转义”,即,将反斜杠字符\ 放在它们前面。下表列出了正则表达式中的特殊字符:

特别字符 描述
$ 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。要匹配 $ 字符本身,请使用 $。
( ) 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 ( 和 )。
* 匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*
+ 匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 +。
. 匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 . 。
[ 标记一个中括号表达式的开始。要匹配 [,请使用 \[
? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 ?。
\ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, ‘n’ 匹配字符 ‘n’。’\n’ 匹配换行符。序列 ‘\‘ 匹配 “",而 ‘(‘ 则匹配 “(“。
^ 匹配输入字符串的开始位置,除非在方括号表达式中使用,此时它表示不接受该字符集合。要匹配 ^ 字符本身,请使用 ^。
{ 标记限定符表达式的开始。要匹配 {,请使用 {。

限定符

限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。有 *+?{n}{n,}{n,m} 共6种。

正则表达式的限定符有:

字符 描述
* 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
? 匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 、 “does” 中的 “does” 、 “doxy” 中的 “do” 。? 等价于 {0,1}。
{n} n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,} n 是一个非负整数。至少匹配n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*’。
{n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。

元字符

下表包含了元字符的完整列表以及它们在正则表达式上下文中的行为:

字符 描述
\ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,’n’ 匹配字符 “n”。’\n’ 匹配一个换行符。序列 ‘\‘ 匹配 “" 而 “(“ 则匹配 “(“。
^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。
  •      |匹配前面的子表达式零次或多次。例如,`zo*` 能匹配 "z" 以及 "zoo"。`*` 等价于{0,}。
    
  •      |匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
    
    ? |匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 或 “does” 。? 等价于 {0,1}。
    {n} |n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
    {n,} |n 是一个非负整数。至少匹配n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 'o*'
    {n,m} |m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。
    ? |当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 “oooo”,’o+?’ 将匹配单个 “o”,而 ‘o+’ 将匹配所有 ‘o’。
    . |匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用像”(.|\n)”的模式。
    (pattern) |匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 $0…$9 属性。要匹配圆括号字符,请使用 ‘(‘ 或 ‘)‘。
    (?:pattern)|匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (|) 来组合一个模式的各个部分是很有用。例如, ‘industr(?:y|ies) 就是一个比 ‘industry|industries’ 更简略的表达式。
    (?=pattern)|正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,”Windows(?=95|98|NT|2000)”能匹配”Windows2000”中的”Windows”,但不能匹配”Windows3.1”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
    (?!pattern)|正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如”Windows(?!95|98|NT|2000)”能匹配”Windows3.1”中的”Windows”,但不能匹配”Windows2000”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
    (?<=pattern)|反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,”(?<=95|98|NT|2000)Windows”能匹配”2000Windows”中的”Windows”,但不能匹配”3.1Windows”中的”Windows”。
    (?<!pattern)|反向否定预查,与正向否定预查类似,只是方向相反。例如”(?<!95|98|NT|2000)Windows”能匹配”3.1Windows”中的”Windows”,但不能匹配”2000Windows”中的”Windows”。
    x|y |匹配 x 或 y。例如,’z|food’ 能匹配 “z” 或 “food”。’(z|f)ood’ 则匹配 “zood” 或 “food”。
    [xyz] |字符集合。匹配所包含的任意一个字符。例如, '[abc]' 可以匹配 “plain” 中的 ‘a’。
    [^xyz] |负值字符集合。匹配未包含的任意字符。例如, '[^abc]' 可以匹配 “plain” 中的’p’、’l’、’i’、’n’。
    [a-z] |字符范围。匹配指定范围内的任意字符。例如,'[a-z]' 可以匹配 ‘a’ 到 ‘z’ 范围内的任意小写字母字符。
    [^a-z] |负值字符范围。匹配任何不在指定范围内的任意字符。例如,'[^a-z]' 可以匹配任何不在 ‘a’ 到 ‘z’ 范围内的任意字符。
    \b |匹配一个单词边界,也就是指单词和空格间的位置。例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
    \B |匹配非单词边界。’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
    \cx |匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
    \d |匹配一个数字字符。等价于 [0-9]
    \D |匹配一个非数字字符。等价于 [^0-9]
    \f |匹配一个换页符。等价于 \x0c 和 \cL。
    \n |匹配一个换行符。等价于 \x0a 和 \cJ。
    \r |匹配一个回车符。等价于 \x0d 和 \cM。
    \s |匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]
    \S |匹配任何非空白字符。等价于 [^ \f\n\r\t\v]
    \t |匹配一个制表符。等价于 \x09 和 \cI。
    \v |匹配一个垂直制表符。等价于 \x0b 和 \cK。
    \w |匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'
    \W |匹配非字母、数字、下划线。等价于 '[^A-Za-z0-9_]'
    \xn |匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,’\x41’ 匹配 “A”。’\x041’ 则等价于 ‘\x04’ & “1”。正则表达式中可以使用 ASCII 编码。
    \num |匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,’(.)\1’ 匹配两个连续的相同字符。
    \n |标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
    \nm | 标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。
    \nml |如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
    \un |匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号。

SecurityHelper 加密解密帮助类

SecurityHelper 帮助类

该帮助类主要实现了 MD5 算法的 加密DES3DESRSA 算法的 加密解密,内部简单实现了一个混淆算法。

MD5 加密算法

MD5 消息摘要算法:全称是 Message-Digest Algorithm,一种被广泛使用的密码散列函数,可以产生出一个 128位16字节)的 散列值hash value),用于确保信息传输完整一致。。

MD5主要用途:

  • 对一段信息生成信息摘要,该摘要对该信息具有唯一性,可以作为数字签名
  • 用于验证文件的有效性(是否有丢失或损坏的数据)
  • 对用户密码的加密
  • 在哈希函数中计算散列值

从上边的主要用途中我们看到,由于算法的某些不可逆特征,在加密应用上有较好的安全性。通过使用 MD5 加密算法,我们输入一个任意长度的字节串,都会生成一个128位 的整数。所以根据这一点 MD5 被广泛的用作密码加密。

SecurityHelper 中调用的方法:

1
string str = SecurityHelper.EncryptByMD5("测试");

DES3DES 加密算法

DES 全称为 Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法。DES 算法的入口参数有三个:KeyDataMode。其中 Key7个字节56位,是 DES算法 的工作密钥;Data8个字节 64位,是要被加密或被解密的数据;ModeDES 的工作方式,有两种:加密或解密。其速度较快,适用于加密大量数据的场合。

SecurityHelper 中调用的方法:

1
2
string encryptStr = SecurityHelper.EncryptByDES("测试");
string str = SecurityHelper.DecryptByDES(encryptStr);

3DES(即 Triple DES)是 DESAES 过渡的加密算法,它使用 3条 56位 的密钥对数据进行 三次加密。是 DES 的一个更安全的变形。它以 DES 为基本模块,通过组合分组方法设计出分组加密算法。比起最初的 DES3DES 更为安全。

SecurityHelper 中调用的方法:

1
2
string encryptStr = SecurityHelper.EncryptByTDES("测试");
string str = SecurityHelper.DecryptByTDES(encryptStr);

RSA 加密算法

RSA 加密算法是一种 非对称 加密算法。在公开密钥加密和电子商业中 RSA 被广泛使用。

RSA 算法是第一个能同时用于加密和数字签名的算法,也易于理解和操作。RSA 是被研究得最广泛的公钥算法,从提出到现今的三十多年里,经历了各种攻击的考验,逐渐为人们接受,截止 2017年 被普遍认为是最优秀的公钥方案之一。

SecurityHelper 中调用的方法:

1
2
3
4
5
// 生成公钥与私钥
var kv = SecurityHelper.GeneralRSAKeys();

string encryptStr = SecurityHelper.EncryptByRSAFromXmlString("测试", kv.Key);
string str = SecurityHelper.DecryptByRSAFromXmlString(encryptStr, kv.Value);

混淆反混淆

当需求对数据的传输有不高的安全加密,且加密的时间复杂度越低越好,这时我们可以使用简单的混淆算法,SecurityHelper 实现了一个简单的混淆算法供开发人员调用。

1
2
string encryptStr = SecurityHelper.MixUp("测试");
string str = SecurityHelper.ClearUp(encryptStr);

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace System
{
/// <summary>
/// 加密安全帮助类
/// </summary>
public class SecurityHelper
{
/// <summary>
/// MD5加密字符串 (不可逆)
/// </summary>
/// <param name="text">需要加密的内容</param>
/// <param name="key">加密密钥,防止简单密码被破解</param>
/// <param name="binaryStyle">加密后结果内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>加密结果</returns>
public static string EncryptByMD5(string text, string key = default(string), bool binaryStyle = true)
{
MD5 md5 = MD5.Create();
byte[] bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(text + key));
return binaryStyle ? BitConverter.ToString(bytes) : Convert.ToBase64String(bytes);
}

/// <summary>
/// DES加密字符串 (可逆)
/// </summary>
/// <param name="text">待加密的字符串</param>
/// <param name="key">DES加密的私钥,必须是字节长度8位的字符串,否则会补0。</param>
/// <param name="iv">DES加密偏移量,必须是>=8位长的字符串,否则会补0。</param>
/// <param name="binaryStyle">加密后结果内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>加密后的字符串</returns>
public static string EncryptByDES(string text, string key = "", string iv = "", bool binaryStyle = true)
{
List<byte> bKey = Encoding.UTF8.GetBytes(key).ToList();
for (int i = bKey.Count; i < 8; i++)
{
bKey.Add((byte)i);
}
bKey = bKey.GetRange(0, 8);
List<byte> bIV = Encoding.UTF8.GetBytes(iv).ToList();
for (int i = bIV.Count; i < 8; i++)
{
bIV.Add((byte)i);
}
using (DESCryptoServiceProvider des = new DESCryptoServiceProvider())
using (MemoryStream ms = new MemoryStream())
{
byte[] inData = Encoding.UTF8.GetBytes(text);
using (CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(bKey.ToArray(), bIV.ToArray()), CryptoStreamMode.Write))
{
cs.Write(inData, 0, inData.Length);
cs.FlushFinalBlock();
}
return binaryStyle ? BitConverter.ToString(ms.ToArray()) : Convert.ToBase64String(ms.ToArray());
}
}

/// <summary>
/// DES解密字符串
/// </summary>
/// <param name="text">待解密的字符串</param>
/// <param name="key">DES加密的私钥,必须是字节长度8位的字符串,否则会补0。</param>
/// <param name="iv">DES加密偏移量,必须是>=8位长的字符串,否则会补0。</param>
/// <param name="binaryStyle">解密内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>解密后的字符串</returns>
public static string DecryptByDES(string text, string key = "", string iv = "", bool binaryStyle = true)
{
List<byte> bKey = Encoding.UTF8.GetBytes(key).ToList();
for (int i = bKey.Count; i < 8; i++)
{
bKey.Add((byte)i);
}
bKey = bKey.GetRange(0, 8);
List<byte> bIV = Encoding.UTF8.GetBytes(iv).ToList();
for (int i = bIV.Count; i < 8; i++)
{
bIV.Add((byte)i);
}
using (DESCryptoServiceProvider des = new DESCryptoServiceProvider())
using (MemoryStream ms = new MemoryStream())
{
byte[] inData = binaryStyle ? text.GetBytesFromBitConverter() : Convert.FromBase64String(text);
using (CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(bKey.ToArray(), bIV.ToArray()), CryptoStreamMode.Write))
{
cs.Write(inData, 0, inData.Length);
cs.FlushFinalBlock();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
}

/// <summary>
/// TDES加密字符串 (可逆)
/// </summary>
/// <param name="text">待加密的字符串</param>
/// <param name="key">TDES加密的私钥,必须是字节长度24位的字符串,否则会自动补位。</param>
/// <param name="iv">TDES加密偏移量,必须是字节长度>=8位的字符串,否则会自动补位。</param>
/// <param name="binaryStyle">加密后结果内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>加密后的字符串</returns>
public static string EncryptByTDES(string text, string key = "", string iv = "", bool binaryStyle = true)
{
List<byte> bKey = Encoding.UTF8.GetBytes(key).ToList();
for (int i = bKey.Count; i < 24; i++)
{
bKey.Add((byte)i);
}
bKey = bKey.GetRange(0, 24);
List<byte> bIV = Encoding.UTF8.GetBytes(iv).ToList();
for (int i = bIV.Count; i < 8; i++)
{
bIV.Add((byte)i);
}
using (TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider())
using (MemoryStream ms = new MemoryStream())
{
byte[] inData = Encoding.UTF8.GetBytes(text);
using (CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(bKey.ToArray(), bIV.ToArray()), CryptoStreamMode.Write))
{
cs.Write(inData, 0, inData.Length);
cs.FlushFinalBlock();
}
return binaryStyle ? BitConverter.ToString(ms.ToArray()) : Convert.ToBase64String(ms.ToArray());
}
}

/// <summary>
/// TDES解密字符串
/// </summary>
/// <param name="text">待解密的字符串</param>
/// <param name="key">TDES加密的私钥,必须是字节长度24位的字符串,否则会自动补位。</param>
/// <param name="iv">TDES加密偏移量,必须是字节长度>=8位的字符串,否则会自动补位。</param>
/// <param name="binaryStyle">解密内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>解密后的字符串</returns>
public static string DecryptByTDES(string text, string key = "", string iv = "", bool binaryStyle = true)
{
List<byte> bKey = Encoding.UTF8.GetBytes(key).ToList();
for (int i = bKey.Count; i < 24; i++)
{
bKey.Add((byte)i);
}
bKey = bKey.GetRange(0, 24);
List<byte> bIV = Encoding.UTF8.GetBytes(iv).ToList();
for (int i = bIV.Count; i < 8; i++)
{
bIV.Add((byte)i);
}
using (TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider())
using (MemoryStream ms = new MemoryStream())
{
byte[] inData = binaryStyle ? text.GetBytesFromBitConverter() : Convert.FromBase64String(text);
using (CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(bKey.ToArray(), bIV.ToArray()), CryptoStreamMode.Write))
{
cs.Write(inData, 0, inData.Length);
cs.FlushFinalBlock();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
}

/// <summary>
/// 获取RSA加密公钥和私钥
/// </summary>
/// <returns>key:公钥 value:公钥和私钥</returns>
public static KeyValuePair<string, string> GeneralRSAKeys()
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
return new KeyValuePair<string, string>(rsa.ToXmlString(false), rsa.ToXmlString(true));
}
}

/// <summary>
/// RSA加密字符串 (可逆)
/// </summary>
/// <param name="text">待加密的字符串</param>
/// <param name="name">密钥容器的名称。</param>
/// <param name="binaryStyle">加密后结果内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>加密后的字符串</returns>
public static string EncryptByRSA(string text, string name, bool binaryStyle = true)
{
CspParameters param = new CspParameters() { KeyContainerName = name };
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(param))
{
byte[] plaindata = Encoding.UTF8.GetBytes(text);
byte[] encryptdata = rsa.Encrypt(plaindata, false);
return binaryStyle ? BitConverter.ToString(encryptdata) : Convert.ToBase64String(encryptdata);
}
}

/// <summary>
/// RSA解密字符串
/// </summary>
/// <param name="text">待解密的字符串</param>
/// <param name="name">密钥容器的名称。</param>
/// <param name="binaryStyle">解密内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>解密后的字符串</returns>
public static string DecryptByRSA(string text, string name, bool binaryStyle = true)
{
CspParameters param = new CspParameters() { KeyContainerName = name };
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(param))
{
byte[] encryptdata = binaryStyle ? text.GetBytesFromBitConverter() : Convert.FromBase64String(text);
byte[] decryptdata = rsa.Decrypt(encryptdata, false);
return Encoding.UTF8.GetString(decryptdata);
}
}

/// <summary>
/// RSA加密字符串 (可逆)
/// </summary>
/// <param name="text">待加密的字符串</param>
/// <param name="xml">含有密钥信息的xml字符串。</param>
/// <param name="binaryStyle">加密后结果内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>加密后的字符串</returns>
public static string EncryptByRSAFromXmlString(string text, string xml, bool binaryStyle = true)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.FromXmlString(xml);
byte[] plaindata = Encoding.UTF8.GetBytes(text);
byte[] encryptdata = rsa.Encrypt(plaindata, false);
return binaryStyle ? BitConverter.ToString(encryptdata) : Convert.ToBase64String(encryptdata);
}
}

/// <summary>
/// RSA解密字符串
/// </summary>
/// <param name="text">待解密的字符串</param>
/// <param name="xml">含有密钥信息的xml字符串。</param>
/// <param name="binaryStyle">解密内容样式。true:HEX格式加密结果,含“-”;Base64格式加密结果。</param>
/// <returns>解密后的字符串</returns>
public static string DecryptByRSAFromXmlString(string text, string xml, bool binaryStyle = true)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.FromXmlString(xml);
byte[] encryptdata = binaryStyle ? text.GetBytesFromBitConverter() : Convert.FromBase64String(text);
byte[] decryptdata = rsa.Decrypt(encryptdata, false);
return Encoding.UTF8.GetString(decryptdata);
}
}

/// <summary>
/// 用时间简单混淆
/// </summary>
/// <param name="text">原始文本</param>
/// <param name="timestampLength">混淆用时间戳长度</param>
/// <returns>混淆后文本</returns>
public static string MixUp(string text,int timestampLength = 36)
{
var timestamp = Guid.NewGuid().ToString();
var count = text.Length + timestampLength;
var sbd = new StringBuilder(count);
int j = 0;
int k = 0;
for (int i = 0; i < count; i++)
{
if (j < timestampLength && k < text.Length)
{
if (i % 2 == 0)
{
sbd.Append(text[k]);
k++;
}
else
{
sbd.Append(timestamp[j]);
j++;
}
}
else if (j >= timestampLength)
{
sbd.Append(text[k]);
k++;
}
else if (k >= text.Length)
{
break;
}
}

return sbd.ToString();
}

/// <summary>
/// 简单反混淆
/// </summary>
/// <param name="text">需要执行反混淆的文本</param>
/// <param name="timestampLength">混淆用时间戳长度</param>
/// <returns>原始文本</returns>
public static string ClearUp(string text, int timestampLength = 36)
{
var sbd = new StringBuilder();
int j = 0;
for (int i = 0; i < text.Length; i++)
{
if (i % 2 == 0)
{
sbd.Append(text[i]);
}
else
{
j++;
}

if (j > timestampLength)
{
sbd.Append(text.Substring(i));
break;
}
}

return sbd.ToString();
}
}
}

注意: 刚刚发现部分代码调用了我自己封装的一个扩展方法的文件,主要是字符串与二进制流转换的处理,可以根据方法名含义自行调整对应方法,后面我也会将这部分代码上传上来,这里就偷个懒不做修改了。

创建支持多个dotnet版本的类库项目

如何创建支持支持多个 dotnet 版本的类库项目

开发中,经常会遇到需要所开发的类库同时支持 net40net451netstandart2.0 等版本。

随意打开一些常用的开源项目比如“Dapper”就会发现,项目并不会针对不同的 dotnet 版本,创建不同分支,而是一套代码支持了多个不同的 dotnet 版本。

而针对不同版本的语言特性,只需要使用 #if 预处理指令进行处理即可,可以节约了我们的开发成本,方便我们统一的管理项目源码。

以下部分的内容是基于 Visual Studio 2017 进行的实践,建议安装最新的VS版本。

项目文件 *.csproj

创建项目

注意:创建的项目类型必须是 .NET Standard

编辑 *.csproj 文件

在项目上右键,右键菜单中便有 编辑 *.csproj 的选项:

如果是新建的 类库(.NET Standard) 项目,*.csproj 文件内容大概是:

1
2
3
4
5
6
7
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

</Project>

正常一个扩展后的内容是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Test</PackageId>
<PackageTags>test1;test2</PackageTags>
<Authors>John Sun</Authors>
<TargetFrameworks>net40;net451;netstandard2.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="NLog" Version="4.5.11" />
<PackageReference Include="NLog.Config" Version="4.5.11" />
<PackageReference Include="NPOI" Version="2.4.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net40' OR '$(TargetFramework)' == 'net451'">
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Web" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
<PackageReference Include="dapper_net40" Version="1.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="dapper_net40" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Test\TestCore.csproj" />
</ItemGroup>
</Project>

需要关注的配置节点

  • PropertyGroup: 配置项目的基本信息
    • TargetFrameworks: 配置项目对框架元包的引用,注意默认一个引用时是 TargetFramework,需要修改为复数
    • PackageId: 指定生成包的名称
    • PackageTags: 标记
    • PackageVersion: 指定生成的包所具有的版本
    • Authors: 以分号分隔的包作者列表
    • OutputType: 可以使用 <OutputType>Exe</OutputType> 指定项目为控制台项目
    • ……
  • ItemGroup: 向项目添加依赖项
    • PackageReference: nuget程序包
    • Reference: 程序集引用
    • ProjectReference: 项目引用

C# 预处理器指令

我们在开发中可能会遇到部分代码在不同的 dotnet 版本上有不同的实现,或者部分程序类型/方法等只能在指定的 dotnet 版本上使用,这个时候我们就可以使用 C# 的预处理指令 #if

针对不同的目标框架使用最新的 API:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass
{
static void Main()
{
#if NET40
WebClient _client = new WebClient();
#else
HttpClient _client = new HttpClient();
#endif
}
//...
}

在目标框架下屏蔽一个方法

1
2
3
4
5
6
7
8
9
10
public class MyClass
{
#if !NET20
public async void OpenAsync()
{
//...
}
#endif
//...
}

参考

线程与线程池

Thread

  • Thread 是前台线程,启动后需要执行完成后才会退出。但是可以通过 IsBackground 将其设置为后台线程,程序退出该线程也会立即退出。
  • 如果希望等待线程执行完成后再继续执行,可以使用 Join() 方法。
  • Thread 没有回调,也没有返回值。

ThreadPool

CLR线程池:

管理线程开销最好的方式:

  • 尽量少的创建线程并且能将线程反复利用(线程池初始化时没有线程,有程序请求线程则创建线程);
  • 最好不要销毁而是挂起线程达到避免性能损失(线程池创建的线程完成任务后以挂起状态回到线程池中,等待下次请求);
  • 通过一个技术达到让应用程序一个个执行工作,类似于一个队列(多个应用程序请求线程池,线程池会将各个应用程序排队处理);
  • 如果某一线程长时间挂起而不工作的话,需要彻底销毁并且释放资源(线程池自动监控长时间不工作的线程,自动销毁);
  • 如果线程不够用的话能够创建线程,并且用户可以自己定制最大线程创建的数量(当队列过长,线程池里的线程不够用时,线程池不会坐视不理);

实现:

  • C#2.0 时支持,实现为享元模式、单例模式。
  • CLR线程池并不会在CLR初始化时立即建立线程,而是在应用程序要创建线程来运行任务时,线程池才初始化一个线程。
  • 线程池初始化时是没有线程的,线程池里的。线程的初始化与其他线程一样,但是在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。

注意:通过CLR线程池所建立的线程总是默认为后台线程,优先级数为ThreadPriority.Normal。

工作者线程与I/O线程

CLR 线程池分为工作者线程( workerThreads )与 I/O 线程( completionPortThreads )两种:

  • 工作者线程:主要用作管理 CLR 内部对象的运作,通常用于计算密集的任务。
  • I/O ( Input/ Output )线程主要用于与外部系统交互信息,如输入输出, CPU 仅需在任务开始的时候,将任务的参数传递给设备,然后启动硬件设备即可。等任务完成的时候, CPU 收到一个通知,一般来说是一个硬件的中断信号,此时 CPU 继续后继的处理工作。在处理过程中, CPU 是不必完全参与处理过程的,如果正在运行的线程不交出 CPU 的控制权,那么线程也只能处于等待状态,即使操作系统将当前的 CPU 调度给其他线程,此时线程所占用的空间还是被占用,而并没有 CPU 处理这个线程,可能出现线程资源浪费的问题。如果这是一个网络服务程序,每一个网络连接都使用一个线程管理,可能出现大量线程都在等待网络通信,随着网络连接的不断增加,处于等待状态的线程将会很消耗尽所有的内存资源。可以考虑使用线程池解决这个问题。

线程池的最大值一般默认为1000、2000。当大于此数目的请求时,将保持排队状态,直到线程池里有线程可用。

使用 CLR 线程池的工作者线程一般有两种方式:

  • 通过 ThreadPool.QueueUserWorkItem() 方法;
  • 通过委托;

注意:不论是通过ThreadPool.QueueUserWorkItem()还是委托,调用的都是线程池里的线程。

ThreadPool 类常用方法

通过以下两个方法可以读取和设置CLR线程池中工作者线程与I/O线程的最大线程数:

  • ThreadPool.GetMax(out in workerThreads,out int completionPortThreads)
  • ThreadPool.SetMax(int workerThreads,int completionPortThreads)

若想测试线程池中有多少线程正在投入使用,可以通过 ThreadPool.GetAvailableThreads(out in workThreads,out int conoletionPortThreads) 方法。

方法 说明
GetAvailableThreads 剩余空闲线程数
GetMaxThreads 最多可用线程数,所有大于此数目的请求将保持排队状态,直到线程池线程变为可用
GetMinThreads 检索线程池在新请求预测中维护的空闲线程数。
QueueUserWorkItem 启动线程池里得一个线程(队列的方式,如线程池暂时没空闲线程,则进入队列排队)
SetMaxThreads 设置线程池中的最大线程数
SetMinThreads 设置线程池最少需要保留的线程数

各种调用线程池线程的方法

通过 QueueUserWorkItem 启动工作者线程

ThreadPool 线程池中有两个重载的静态方法可以直接启动工作者

1
2
ThreadPool.QueueUserWorkItem(waitCallback);
ThreadPool.QueueUserWorkItem(waitCallback,Object);

通过 ThreadPool.QueueUserWork 启动工作者线程非常方便,但是 WaitCallback 委托指向的必须是一个带有 object 参数的无返回值方法。所以这个方法启动的工作者线程仅仅适合于带单个参数和无返回值的情况。如果要传递多个参数和要有返回值,那就只有通过委托。

BeginInvokeEndInvoke 委托异步调用线程

  1. 建立一个委托对象,通过 IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state) 异步调用委托方法, BeginInvoke 方法除最后的两个参数外,其他参数都是与方法参数相对应的;
  2. 利用 EndInvoke ( IAsyncResult –上一步 BeginInvoke 返回的对象)方法就可以结束异步操作,获取委托的运行结果;

缺点:不知道异步操作什么时候执行完,什么时候开始调用EndInvoke,因为一旦EndInvoke主线程就会处于阻塞等待状态。

IAsyncResult 轮询

克服上面提到的缺点,可以好好利用 IAsyncResult 提高主线程的工作性能:

  • IsCompleted 属性:获取异步操作是否已完成;
  • WaitOne :判断单个异步线程是否完成;
  • WaitAny :判断是否异步线程是否有指定数量个已完成;
  • WaitAll :判断是否所有的异步线程已完成;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Func<string, int, string> func = (name, age) =>
{
Thread.Sleep(20000);
return $"My name is {name}, I'm {age} years old.";
};
IAsyncResult asyncResult = func.BeginInvoke("Jane", 12, null, null);
//IAsyncResult.IsCompleted属性
while (!asyncResult.IsCompleted)
{
Thread.Sleep(500);
Console.WriteLine("*************WAITING*************");
}
////AsyncWaitHandle.WaitOne()方法
//while (!asyncResult.AsyncWaitHandle.WaitOne(200))
//{
// Console.WriteLine("*************WAITING*************");
//}
////WaitHandle.WaitAny()方法
//WaitHandle[] waitHandleList1 = new WaitHandle[] { asyncResult.AsyncWaitHandle };
//while (WaitHandle.WaitAny(waitHandleList1, 200) > 0)
//{
// Console.WriteLine("*************WAITING*************");
//}
////WaitHandle.WaitAll()方法
//WaitHandle[] waitHandleList2 = new WaitHandle[] { asyncResult.AsyncWaitHandle };
//while (WaitHandle.WaitAll(waitHandleList2, 200))
//{
// Console.WriteLine("*************WAITING*************");
//}
Console.WriteLine(func.EndInvoke(asyncResult));

IAsyncResult 回调函数

使用轮询方式来检测异步方法的状态非常麻烦,而且影响了主线程,效率不高。我们可以使用IAsyncResult对象,当异步线程完成了就直接调用实现定义好的处理函数。

1
2
3
4
5
6
7
8
9
10
11
Func<string, int, string> func = (name, age) =>
{
Thread.Sleep(2000);
return $"My name is {name}, I'm {age} years old.";
};
IAsyncResult asyncResult = null;
AsyncCallback callback = (t) =>
{
Console.WriteLine(func.EndInvoke(asyncResult));
};
asyncResult = func.BeginInvoke("Jane", 12, callback, null);

注意:

  • 回调函数依然是在辅助线程中执行的,这样就不会影响主线程的运行。
  • 线程池的线程默认是后台线程。但是如果主线程比辅助线程优先完成,那么程序已经卸载,回调函数未必会执行。如果不希望丢失回调函数中的操作,要么把异步线程设为前台线程,要么确保主线程将比辅助线程迟完成。

ManualResetEvent

常用方法: Set()ReSet()WaitOne()

  • Set() : 用于向 ManualResetEvent 发送信号,使其取消阻塞状态(唤醒进程)或者开始阻塞进程,这基于 ManualResetEvent 的初始状态。
  • ReSet() : 将 ManualResetEvent 的状态重置至初始状态(即使用 Set() 方法之前的状态)。
  • WaitOne() : 使 ManualResetEvent 进入阻塞状态,开始等待唤醒信号。如果有信号,则不会阻塞,直接通过。
  • 信号 : 当 new ManualResetEvent(bool arg) 时, arg 参数就是信号状态,假如为 false ,则表示当前无信号,如果为 true ,则有信号。

异步多线程

基本概念

进程

首先打开任务管理器,可以查看电脑当前运行的进程。

从任务管理器里面可以看到当前所有正在运行的进程。

那么究竟什么是进程呢?

进程( Process )是 Windows 系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。

线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行, Windows 系统就是利用进程把工作划分为多个独立的区域的。

进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。

线程

在任务管理器, 性能 -> CPU 中可以查看当前电脑当前运行的线程数量。

线程( Thread )是进程中的基本执行单元,是操作系统分配 CPU 时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。

.NET 应用程序中,都是以 Main() 方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由 CPU 寄存器、调用栈和线程本地存储器( Thread Local StorageTLS )组成的。

CPU 寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据, TLS 主要用于存放线程的状态信息。

多线程

多线程的优缺点

多线程的优点:

  • 可以同时完成多个任务;
  • 可以使程序的响应速度更快;
  • 可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;
  • 可以随时停止任务;
  • 可以设置每个任务的优先级以优化程序性能。

那么可能有人会问:为什么可以多线程执行呢?

总结起来有下面两方面的原因:

  • CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
  • 目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是多个CPU在同一时刻就可以运行多个线程。

然而,多线程虽然有很多优点,但是也必须认识到多线程可能存在影响系统性能的不利方面,才能正确使用线程。

不利方面主要有如下几点:

  • 线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。
  • 多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。
  • 线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
  • 线程太多会导致控制太复杂,最终可能造成很多程序缺陷。

多线程

当启动一个可执行程序时,将创建一个主线程。在默认的情况下, C# 程序具有一个线程,此线程执行程序中以 Main 方法开始和结束的代码, Main() 方法直接或间接执行的每一个命令都有默认线程(主线程)执行,当 Main() 方法返回时此线程也将终止。

一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码。在 C# 中,线程是使用 Thread 类处理的,该类在 System.Threading 命名空间中。

使用 Thread 类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么。通过实例化一个 Thread 类的对象就可以创建一个线程。

创建新的 Thread 对象时,将创建新的托管线程。 Thread 类接收一个 ThreadStart 委托或 ParameterizedThreadStart 委托的构造函数,该委托包装了调用 Start 方法时由新线程调用的方法,示例代码如下:

1
2
Thread thread=new Thread(new ThreadStart(method));//创建线程
thread.Start(); //启动线程

上面代码实例化了一个 Thread 对象,并指明将要调用的方法 method() ,然后启动线程。 ThreadStart 委托中作为参数的方法不需要参数,并且没有返回值。 ParameterizedThreadStart 委托一个对象作为参数,利用这个参数可以很方便地向线程传递参数,示例代码如下:

1
2
Thread thread=new Thread(new ParameterizedThreadStart(method));//创建线程
thread.Start(3); //启动线程

创建多线程的步骤:

  1. 编写线程所要执行的方法
  2. 实例化 Thread 类,并传入一个指向线程所要执行方法的委托。(这时线程已经产生,但还没有运行)
  3. 调用 Thread 实例的 Start 方法,标记该线程可以被 CPU 执行了,但具体执行时间由 CPU 决定

System.Threading.Thread

Thread类是是控制线程的基础类,位于System.Threading命名空间下,具有4个重载的构造函数:

  • Thread(ParameterizedThreadStart) 初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托。要执行的方法是有参的。
  • Thread(ParameterizedThreadStart, Int32) 初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托,并指定线程的最大堆栈大小。
  • Thread(ThreadStart) 初始化 Thread 类的新实例。要执行的方法是无参的。
  • Thread(ThreadStart, Int32) 初始化 Thread 类的新实例,指定线程的最大堆栈大小。
  • ThreadStart 是一个无参的、返回值为 void 的委托。
  • ParameterizedThreadStart 是一个有参的、返回值为 void 的委托。

注意: ParameterizedThreadStart 委托的参数类型必须是 Object 的。如果使用的是不带参数的委托,不能使用带参数的 Start 方法运行线程,否则系统会抛出异常。但使用带参数的委托,可以使用 thread.Start() 来运行线程,这时所传递的参数值为 null

线程的常用属性

  • CurrentContext 获取线程正在其中执行的当前上下文。
  • CurrentThread 获取当前正在运行的线程。
  • ExecutionContext 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。
  • IsAlive 获取一个值,该值指示当前线程的执行状态。
  • IsBackground 获取或设置一个值,该值指示某个线程是否为后台线程。
  • IsThreadPoolThread 获取一个值,该值指示线程是否属于托管线程池。
  • ManagedThreadId 获取当前托管线程的唯一标识符。
  • Name 获取或设置线程的名称。
  • Priority 获取或设置一个值,该值指示线程的调度优先级。
  • ThreadState 获取一个值,该值包含当前线程的状态。

线程的标识符

ManagedThreadId 是确认线程的唯一标识符,程序在大部分情况下都是通过 Thread.ManagedThreadId 来辨别线程的。而Name是一个可变值,在默认时候, Name 为一个空值 Null ,开发人员可以通过程序设置线程的名称,但这只是一个辅助功能。

线程的优先级别

当线程之间争夺 CPU 时间时, CPU 按照线程的优先级给予服务。高优先级的线程可以完全阻止低优先级的线程执行。

.NET 为线程设置了 Priority 属性来定义线程执行的优先级别,里面包含5个选项,其中 Normal 是默认值。除非系统有特殊要求,否则不应该随便设置线程的优先级别。

  • Lowest 可以将 Thread 安排在具有任何其他优先级的线程之后。
  • BelowNormal 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。
  • Normal 默认选择。可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前。
  • AboveNormal 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。
  • Highest 可以将 Thread 安排在具有任何其他优先级的线程之前。

线程的状态

通过 ThreadState 可以检测线程是处于 UnstartedSleepingRunning 等等状态,它比 IsAlive 属性能提供更多的特定信息。

前面说过,一个应用程序域中可能包括多个上下文,而通过 CurrentContext 可以获取线程当前的上下文。

CurrentThread 是最常用的一个属性,它是用于获取当前运行的线程。

System.Threading.Thread 的方法

Thread 中包括了多个方法来控制线程的创建、挂起、停止、销毁,以后来的例子中会经常使用。

  • Abort() 终止本线程。
  • GetDomain() 返回当前线程正在其中运行的当前域。
  • GetDomainId() 返回当前线程正在其中运行的当前域Id。
  • Interrupt() 中断处于 WaitSleepJoin 线程状态的线程。
  • Join() 已重载。 阻塞调用线程,直到某个线程终止时为止。
  • Resume() 继续运行已挂起的线程。
  • Start() 执行本线程。
  • Suspend() 挂起当前线程,如果当前线程已属于挂起状态则此不起作用
  • Sleep() 把正在运行的线程挂起一段时间。

前台线程和后台线程

  • 前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程。
  • 后台线程:只要所有的前台线程结束,后台线程自动结束。通过 Thread.IsBackground 设置后台线程。必须在调用 Start 方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。通过 BeginXXX 方法运行的线程都是后台线程。后台线程一般用于处理不重要的事情,应用程序结束时,后台线程是否执行完成对整个应用程序没有影响。如果要执行的事情很重要,需要将线程设置为前台线程。

线程同步

所谓同步:是指在某一时刻只有一个线程可以访问变量。

如果不能确保对变量的访问是同步的,就会产生错误。

C# 为同步访问变量提供了一个非常简单的方式,即使用 C# 语言的关键字 Lock ,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在 C# 中,关键字 Lock 定义如下:

1
2
3
4
Lock(expression)
{
statement_block
}

expression 代表你希望跟踪的对象:如果你想保护一个类的实例,一般地,你可以使用 this ;如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而 statement_block 就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

跨线程访问

创建窗体应用程序,增加一个测试按钮和一个文本框,点击“测试”,创建一个线程,从0循环到10000给文本框赋值,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void btn_Test_Click(object sender, EventArgs e)
{
//创建一个线程去执行这个方法:创建的线程默认是前台线程
Thread thread = new Thread(new ThreadStart(Test));
//Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定
//将线程设置为后台线程
thread.IsBackground = true;
thread.Start();
}
private void Test()
{
for (int i = 0; i < 10000; i++)
{
this.textBox1.Text = i.ToString();
}
}

运行结果会报错:线程间操作无效:不是创建“textBox1”的线程访问它。

产生错误的原因:textBox1是由主线程创建的,thread线程是另外创建的一个线程,在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。

解决方案:

  1. 在窗体的加载事件中,将 C# 内置控件( Control )类的 CheckForIllegalCrossThreadCalls 属性设置为 false ,屏蔽掉 C# 编译器对跨线程调用的检查。
1
2
3
4
5
private void Form1_Load(object sender, EventArgs e)
{
//取消跨线程的访问
Control.CheckForIllegalCrossThreadCalls = false;
}

使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合 .NET 的安全规范),在产品软件的开发中,此类情况是不允许的。如果要在遵守.NET安全标准的前提下,实现从一个线程成功地访问另一个线程创建的空间,要使用 C# 的方法回调机制。

  1. 使用回调函数

回调实现的一般过程: C# 的方法回调机制,也是建立在委托基础上的,下面给出它的典型实现过程。

  • 定义、声明回调。
1
2
3
4
//定义回调
private delegate void DoSomeCallBack(Type para);
//声明回调
DoSomeCallBack doSomaCallBack;

可以看出,这里定义声明的“回调”( doSomaCallBack )其实就是一个委托。

  • 初始化回调方法。
1
doSomeCallBack = new DoSomeCallBack(DoSomeMethod);

所谓“初始化回调方法”实际上就是实例化刚刚定义了的委托,这里作为参数的 DoSomeMethod 称为“回调方法”,它封装了对另一个线程中目标对象(窗体控件或其他类)的操作代码。

  • 触发对象动作
1
Opt obj.Invoke(doSomeCallBack, arg);

其中 Opt obj 为目标操作对象,在此假设它是某控件,故调用其 Invoke 方法。 Invoke 方法签名为:

1
object Control.Invoke(Delegate method, params object[] args);

它的第一个参数为委托类型,可见“触发对象动作”的本质,就是把委托 doSomeCallBack 作为参数传递给控件的 Invoke 方法,这与委托的使用方式是一模一样的。
最终作用于对象 Opt obj 的代码是置于回调方法体 DoSomeMethod() 中的,如下所示:

1
2
3
4
5
private void DoSomeMethod(type para)
{
//方法体
Opt obj.someMethod(para);
}

如果不用回调,而是直接在程序中使用 Opt obj.someMethod(para); ,则当对象 Opt obj 不在本线程(跨线程访问)时就会发生上面所示的错误。

从以上回调实现的一般过程可知: C# 的回调机制,实质上是委托的一种应用。在 C# 网络编程中,回调的应用是非常普遍的,有了方法回调,就可以在 .NET 上写出线程安全的代码了。

使用方法回调,实现给文本框赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
namespace MultiThreadDemo
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//定义回调
private delegate void setTextValueCallBack(int value);
//声明回调
private setTextValueCallBack setCallBack;
private void btn_Test_Click(object sender, EventArgs e)
{
//实例化回调
setCallBack = new setTextValueCallBack(SetValue);
//创建一个线程去执行这个方法:创建的线程默认是前台线程
Thread thread = new Thread(new ThreadStart(Test));
//Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定
//将线程设置为后台线程
thread.IsBackground = true;
thread.Start();
}
private void Test()
{
for (int i = 0; i < 10000; i++)
{
//使用回调
textBox1.Invoke(setCallBack, i);
}
}
/// <summary>
/// 定义回调使用的方法
/// </summary>
/// <param name="value"></param>
private void SetValue(int value)
{
this.textBox1.Text = value.ToString();
}
}
}

终止线程

若想终止正在运行的线程,可以使用 Abort() 方法。

同步和异步

同步和异步是对方法执行顺序的描述。

同步:等待上一行完成计算之后,才会进入下一行。

例如:请同事吃饭,同事说很忙,然后就等着同事忙完,然后一起去吃饭。

异步:不会等待方法的完成,会直接进入下一行,是非阻塞的。

例如:请同事吃饭,同事说很忙,那同事先忙,自己去吃饭,同事忙完了他自己去吃饭。

同步方法和异步方法的区别:

    • 同步方法由于主线程忙于计算,所以会卡住界面。
    • 异步方法由于主线程执行完了,其他计算任务交给子线程去执行,所以不会卡住界面,用户体验性好。
    • 同步方法由于只有一个线程在计算,所以执行速度慢。
    • 异步方法由多个线程并发运算,所以执行速度快,但并不是线性增长的(资源可能不够)。多线程也不是越多越好,只有多个独立的任务同时运行,才能加快速度。
    • 同步方法是有序的。
    • 异步多线程是无序的:启动无序,执行时间不确定,所以结束也是无序的。一定不要通过等待几毫秒的形式来控制线程启动/执行时间/结束。

回调

  • 异步多线程是无序的,可以通过使用回调解决异步多线程是无序的问题。
  • 获取委托异步调用的返回值:使用 EndInvoke() 可以获取委托异步调用的返回值。注意调用该方法会等待异步执行完成,所以在主线程中执行,会强制等待委托执行完成,而在回调中执行因为委托其实已经执行完成,所以此时 EndInvoke() 单纯为了获取委托执行的结果。

面向对象知识点回顾

封装

访问修饰符

  • public :完全公开
  • private :只有类内部能够访问
  • internal :同项目中能够访问
  • protected :类和派生类可以访问
  • protected internal :internal和protected二者的结合

封装

封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。被封装的对象通常被称为抽象数据类型。

意义

封装的意义在于保护或者防止代码(数据)被我们无意中破坏。在面向对象程序设计中数据被看作是一个中心的元素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改。

封装提供了一个有效的途径来保护数据不被意外的破坏。相比我们将数据(用域来实现)在程序中定义为公用的( public )我们将它们( fields )定义为私有的( privat )在很多方面会更好。私有的数据可以用两种方式来间接的控制。

第一种方法,我们使用传统的存、取方法。第二种方法我们用属性(property)。使用属性不仅可以控制存取数据的合法性,同时也提供了“读写”、“只读”、“只写”灵活的操作方法。继承:继承主要实现重用代码,节省开发时间

继承

意义

继承主要实现重用代码,节省开发时间。

继承的规则

继承是可传递的。如果 CB 中派生, B 又从 A 中派生,那么 C 不仅继承了 B 中声明的成员,同样也继承了 A 中的成员。 object 类作为所有类的基类。

派生类应当是对基类的扩展。派生类可以添加新的成员,但不能除去已经继承的成员的定义。

构造函数和析构函数不能被继承。除此之外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。

派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖已继承的成员。但这并不因为这派生类删除了这些成员,只是不能再访问这些成员。

类可以定义虚方法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而实现类可以展示出多态性。

关键字

  • 重载:继承中的重载和类内的成员方法的重载是一样的,只要在子类中新建了和父类中同名的但参数列表不同的成员方法就重载了父类的成员方法,但前题是要能继承该成员方法。
  • 重写:是在子类中重新改写从父类中继承过来的某个方法,用新的方法代替原有的方法,这里要用关键字virtual override。
  • 隐藏:也是在子类中重新改写从父类中继承过来的某个方法,但不是把父类的方法替换掉,而是隐藏起来,要用关键字new。
  • base:可以调用父类的成员方法,除了构造函数和析构函数,派生类将隐式的继承了继承基类的所有成员。也可以显示的调用父类的构造函数来构造子类的成员数据。
  • this:引用类当前的实例,还用于将对象传递到属于其他类的方法。

多态

new 的用法

当派生类与基类型相同的方法使用 new 修饰时,派生类对象转换后,调用的是基类的方法。其实可以理解为,使用 new 关键字后,使得派生类中的方法和基类中的方法成为毫不相关的两个方法,只是它们的名字碰巧相同而已。所以,基类中的方法不管用还是不用 virtual 修饰,也不管访问权限如何,或者是没有,都不会对派生类的方法产生什么影响(只是因为使用了 new 关键字,如果派生类没用 new 关键字从基类继承同名方法,编译器会输出警告)。这可能是设计者有意这么设计的,因为有时候我们就是要达到这种效果。严格的说,不能说通过使用 new 来实现多态,只能说在某些特定的时候碰巧实现了多态的效果。

override 实现多态

真正的多态使用 override 来实现的,在基类中将方法用virtual标记为虚拟方法,再在派生类中用 override 对需要重写的方法修饰进行重写,就可以很简单的实现多态。需要注意的是,要对一个类中一个方法用 override 修饰,该类必须从父类中继承了一个对应的用 virtual 修饰的虚拟方法,否则编译器将报错。如果父类方法用 override 修饰,如果子类继承了该方法,也可以用 override 修饰,多层继承中的多态就是这样实现的。要想终止这种重写,只需重写方法时用 sealed 关键字进行修饰即可。

abstract-override 实现多态

abstract 修饰的抽象方法只是对方法进行了定义,而没有实现,如果一个类包含了抽象方法,那么该类也必须用 abstract 声明为抽象类,一个抽象类是不能被实例化的。对于类中的抽象方法,可以再其派生类中用 override 进行重写,如果不重写,其派生类也要被声明为抽象类。

abstract-override 可以和 virtual-override 一样地实现多态,包括多层继承也是一样的。不同之处在于,包含虚拟方法的类可以被实例化,而包含抽象方法的类不能被实例化。

接口实现多态

接口存在的意义就是为了实现多态,另外接口解决了类的多继承问题,解决了类继承以后体积庞大的问题,接口之间可以实现多继承。接口中的成员必须不能有实现,接口不能实例化,成员不能有访问修饰符(隐式公开),接口中可以有属性,方法,索引器等(属性,索引器本质上也是方法),但不能有字段,接口中的所有方法必须被子类中全部实现(除非子类是抽象类,把接口中的成员标记为抽象的)。一个类只能继承一个父类,但是可以实现多个接口,接口只能决定能干什么,怎么干由类来实现。

注意

  1. 接口是一种规范。为了多态。
  2. 接口不能被实例化。
  3. 接口中的成员不能加“访问修饰符”,接口中的成员访问修饰符为 public ,不能修改。(默认为 public )
  4. 接口中的成员不能有任何实现(“光说不做”,只是定义了一组未实现的成员)。
  5. 接口中只能有方法、属性、索引器、事件,不能有“字段”。
  6. 接口与接口之间可以继承,并且可以多继承。
  7. 实现接口的子类必须实现该接口的全部成员。
  8. 一个类可以同时继承一个类并实现多个接口,如果一个子类同时继承了父类 A ,并实现了接口 IA ,那么语法上A必须写在 IA 的前面。 class MyClass:A,IA{} ,因为类是单继承的。
  9. 当一个抽象类实现接口的时候,如果不想把接口中的成员实现,可以把该成员实现为 abstract 。(抽象类也能实现接口,用 abstrac 标记)
  10. “显示实现接口”,只能通过接口变量来调用 (因为显示实现接口后成员为 private )。

接口和抽象类的区别

  1. 抽象类适用于同一系列,并且有需要继承的成员。

  2. 接口适用于不同系列的类具有相同的动作(行为、动作、方法)。对于不是相同的系列,但具有相同的行为,这个就考虑使用接口。

  3. 接口解决了类不能多继承问题。

  4. 接口定义类的公共行为,抽象类定义类的公共实现。

  5. 一个类只能继承自一个类(抽象类),但是可以同时实现多个接口。

  6. 接口中不能有实现,抽象类中可以有未实现成员也可以有实现的成员。

  7. 接口中未实现的方法必须在子类中实现,抽象类中未实现的成员必须在子类中重写。

    接口和类的异同

  • 不同点:不能直接实例化接口。接口不包含方法的实现。接口可以多继承,类只能单继承。类定义可在不同的源文件之间进行拆分。
  • 相同点:接口、类和结构都可以从多个接口继承。接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。接口和类都可以包含事件、索引器、方法和属性。

C#支持多重继承么?

类之间不支持,接口之间支持。类对接口叫做实现,不叫继承。类是父亲、接口是能力,能有多个能力,但不能有多个父亲。

常见序列化器

常见序列化器

二进制序列化器

命名空间 System.Runtime.Serialization.Formatters.Binary;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//序列化
using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite))
{
BinaryFormatter binaryFormatter = new BinaryFormatter();//创建二进制序列化器
binaryFormatter.Serialize(fileStream, ListPersons);
}
//反序列化
using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
{
BinaryFormatter binaryFormatter = new BinaryFormatter();//创建二进制序列化器
fileStream.Position = 0;//重置流位置
List<Human> list = (List<Human>)binaryFormatter.Deserialize(fileStream);
foreach (var item in list)
{
item.Action();
}
}

Soap 序列化器

命名空间 System.Runtime.Serialization.Formatters.Soap;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//序列化
using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite))
{
SoapFormatter soapFormatter = new SoapFormatter();//创建Soap序列化器
soapFormatter.Serialize(fileStream, ListPersons.ToArray());//不支持泛型
}
//反序列化
using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
{
SoapFormatter soapFormatter = new SoapFormatter();//创建Soap序列化器
fileStream.Position = 0;//重置流位置
List<Human> list = ((Human[])soapFormatter.Deserialize(fileStream)).ToList();//不支持泛型
foreach (var item in list)
{
item.Action();
}
}

XML序列化器

命名空间: System.Xml.Serialization;

XML:标记语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//序列化
using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite))
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(List<Human>));//创建Xml序列化器 需要指定对象的类型
xmlSerializer.Serialize(fileStream, ListPersons);
}
//反序列化
using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(List<Human>));//创建Xml序列化器 需要指定对象的类型
fileStream.Position = 0;//重置流位置
List<Human> list = (List<Human>)xmlSerializer.Deserialize(fileStream);
foreach (var item in list)
{
item.Action();
}
}

Json 序列化器

命名空间:System.Web.Script.Serialization; 或引用 System.Web.Extentsion; 或者 Newtonsoft.Json;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//JavaScriptSerializer序列化
{//内置
JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();//创建Json序列化器
string sJson = javaScriptSerializer.Serialize(ListPersons);
File.WriteAllText(path1, sJson);
}
//JavaScriptSerializer反序列化
{
string sText = File.ReadAllText(path1);
JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();//创建Json序列化器
List<Human> list = javaScriptSerializer.Deserialize<List<Human>>(sText);
foreach (var item in list)
{
item.Action();
}
}
//Newtonsoft.Json序列化
{//nuget
string sJson = JsonConvert.SerializeObject(ListPersons);
File.WriteAllText(path2, sJson);
}
//Newtonsoft.Json反序列化
{
string sText = File.ReadAllText(path1);
List<Human> list = JsonConvert.DeserializeObject<List<Human>>(sText);
foreach (var item in list)
{
item.Action();
}
}

常用 Newtonsoft.Json ,因其效率高于内置的 JavaScriptSerializer ,另如果使用内置序列化器,需要对应序列化的类型指定 [Serializable] 的特性。

C#中的文件操作

文件夹/文件 操作

文件夹/文件检查

主要是 Directory / File / DirectoryInfo / FileInfo 几个类来操作检查。

注:Path是路径字符串的拼接、剪切、检查操作类,并不会执行IO操作。

文件夹/文件新增

1
2
3
Directory.CreateDirectory(path);//新增
File.Create(path);//新增或覆盖
File.AppendText(path);//新增或追加

文件夹新增时,dotnet会自动逐层创建,例如“C:\test1\test2”,“test1”文件夹不存在会先创建“test1”文件夹。

文件夹/文件复制

1
2
//文件夹无法复制
File.Copy(path1, path2);

文件夹/文件移动

1
2
Directory.Move(path1, path2);
File.Move(path1, path2);

文件夹/文件删除

1
2
Directory.Delete(path, true);//第二个参数是指定是否删除目录内文件夹与文件 若不指定默认false 当为false文件夹内存在文件或文件夹会报错
File.Delete(path);

文件写入/读取

常见写入方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//新增或覆盖
using (FileStream fileStream = File.Create(path))
{
string sText = "My name is Li Lei.";
byte[] bytes = Encoding.Default.GetBytes(sText);
fileStream.Write(bytes, 0, bytes.Length);
}
//新增或覆盖
using (FileStream fileStream = File.Create(path))
{
using (StreamWriter streamWriter = new StreamWriter(fileStream))
{
string sText = "My name is Han Meimei.";
streamWriter.Write(sText);
}
}
//新增或追加
using (StreamWriter streamWriter = File.AppendText(path))
{
string sText = "Nice to meet you.";
byte[] bytes = Encoding.Default.GetBytes(sText);
streamWriter.BaseStream.Write(bytes, 0, bytes.Length);
}
//新增或追加
using (StreamWriter streamWriter = File.AppendText(path))
{
string sText = "Nice to meet you too.";
streamWriter.Write(sText);
}

常见读取方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//一次读取
byte[] bytes = File.ReadAllBytes(path);
string sText = File.ReadAllText(path);
string[] aLineText = File.ReadAllLines(path);
IEnumerable<string> listLineText = File.ReadLines(path);
//大文件读取
using (FileStream fileStream = File.OpenRead(path))
{
int length = 5;
int result = 0;
do
{
byte[] bytes = new byte[length];
result = fileStream.Read(bytes, 0, 5);
Console.WriteLine(Encoding.Default.GetString(bytes));
} while (length == result);
}

磁盘操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DriveInfo[] aInfo = DriveInfo.GetDrives();
foreach (var info in aInfo)
{
Console.WriteLine("----------------------------------------------------");
Console.WriteLine("驱动器的名称:" + info.Name);
Console.WriteLine("驱动器根目录:" + info.RootDirectory);
Console.WriteLine("指示驱动器上可用空闲空间总量:" + info.AvailableFreeSpace);
Console.WriteLine("文件系统名称:" + info.DriveFormat);
Console.WriteLine("驱动器类型:" + info.DriveType);
Console.WriteLine("获取一个指示驱动器是否已经准备好:" + info.IsReady);
Console.WriteLine("获取驱动器上可用空闲空间总量:" + info.TotalFreeSpace);
Console.WriteLine("获取驱动器空间总大小:" + info.TotalSize);
Console.WriteLine("获取或设置驱动器的卷标:" + info.VolumeLabel);
}

后记

这部分原本感觉没必要提交上来,因为这部分内容太太基础。

但是实际工作中,经常会遗忘一部分方法怎么使用,所以提交上来供以后查阅。

另外由于经常翻看前辈代码,IO有部分需要特别注意一下:

  • 多线程操作文件读写,一定要加锁,尽量避免使用try处理文件被占用引发的异常;
  • 文件夹创建碰到多层级无需逐层创建,dotnet底层会自动帮我们逐层创建;
  • 记录日志文件,内容使用流操作写文件,直接使用 File.AppendText() 方法即可,而且不用判断文件是否存在,dotnet会在不存在时自动帮我们创建;

先写这些,有些细节以后想到了再补。

匿名类型、扩展方法、var、dynamic

匿名类型

C#3.0版本以后,允许使用new关键字直接创造对象,方便我们在临时使用一特定类型时,无需单独的创建一个类:

1
2
3
4
5
6
7
object user = new
{
Id = 1,
Name = "Kangkang",
Age = 12
};
//Console.WriteLine(user.Id);//无法获取 因为是object类型 该问题可以使用var关键字解决

var 关键字

在方法范围内声明的变量可以具有隐式“类型” var

隐式类型本地变量为强类型,就像用户已经自行声明该类型,但编译器决定类型一样(语法糖)。

声明现有类型

编译器会自行推断左侧变量的类型,如下代码编译器会识别为int类型:

1
var i = 1;

声明匿名类型

创建的匿名类型我们并不知道编译器为我们生成的类型名称,但是我们可以使用var关键字让编译器自己根据编译的结果来推断:

1
2
3
4
5
6
7
var user = new
{
Id = 1,
Name = "Kangkang",
Age = 12
};
Console.WriteLine(user.Id);

特点

  • 必须在定义时初始化,也就是必须是 var s = "abcd" 形式
  • 一但初始化完成,就不能再给变量赋与初始化值类型不同的值
  • var要求是局部变量
  • 使用var定义变量和object不同,它在效率上和使用强类型方式定义变量完全一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
Console.WriteLine("**************** 匿名类型与var ****************");
var user = new
{
Id = 1,
Name = "Kangkang",
Age = 12
};
//user.Name = "Xiaoming";//匿名类型属性只读
Type type = user.GetType();
var properties = type.GetProperties();
foreach (var item in properties)
{
//只读属性反射也无法修改属性值
//if (item.Name == "Name")
//{
// item.SetValue(user, "Xiaoming", null);
//}
Console.WriteLine($"Property name is {item.Name}, value is {item.GetValue(user, null)}.");
}
}

扩展方法

定义

静态类里的静态方法,第一个参数类型前加this关键字

1
2
3
4
5
6
7
8
9
10
public static class ExtensionClass
{
public static Human CheckHuman(this Human human)
{
human = human ?? new Human("Kangkang","China");
human.Name = string.IsNullOrEmpty(human.Name) ? "EmptyName" : human.Name;
human.Country = string.IsNullOrEmpty(human.Country) ? "China" : human.Country;
return human;
}
}

如上,在不修改类型封装的前提下,给类型额外的扩展一个方法(密封类也可以),扩展的方法为实例方法。

使用

既然为实例方法,我们便可以像实例方法一样调用:

1
2
3
4
5
{
Console.WriteLine("**************** 扩展方法的使用 ****************");
Human human = null;
human.CheckHuman().SayHi();
}

注意:如果与实例方法相同,优先实例方法;不能滥用扩展方法,尤其是基类型。

dynamic

简介

dynamic 类型是一种静态类型,但类型为 dynamic 的对象会跳过静态类型检查。 大多数情况下,该对象就像具有类型 object 一样。 在编译时,将假定类型化为 dynamic 的元素支持任何操作。 因此,您不必考虑对象是从 COM API、从动态语言(例如 IronPython)、从 HTML 文档对象模型 (DOM)、从反射还是从程序中的其他位置获取自己的值。 但是,如果代码无效,则在运行时会捕获到错误。

var 对比

一旦被编译,编译期会自动匹配var 变量的实际类型,并用实际类型来替换该变量的申明,这看上去就好像我们在编码的时候是用实际类型进行申明的。
而dynamic被编译后,实际是一个object类型,只不过编译器会对dynamic类型进行特殊处理,让它在编译期间不进行任何的类型检查,而是将类型检查放到了运行期。

var 声明的变量,支持“智能感知”,因为 Visual Studio 能推断出 var 类型的实际类型,而以 dynamic 声明的变量却不支持“智能感知”,因为编译器对其运行期的类型一无所知。对 dynamic 变量使用“智能感知”,会提示“此操作将在运行时解析”。

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dynamic para = new System.Dynamic.ExpandoObject();
para.name = "Kangkang";
para.country = "China";
para.age = 12;

// 可以直接调用
Console.WriteLine(para.name);

// 可以重新赋值其他类型
para.name = 007;
Console.WriteLine(para.name);

// 可以理解其为一个特殊的字典 我们可以使用字典的方式遍历
foreach (var item in para as IDictionary<string, object>)
{
Console.WriteLine($"{item.Key} {item.Value}");
}

// 调用不存在的“属性”编译时不会报错 但是运行时会报错
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:“'System.Dynamic.ExpandoObject' does not contain a definition for 'hobby'”
Console.WriteLine(para.hobby);