文章目录
- [C# 介绍](# 介绍)
- [C# 封装](# 封装)
- [C# 方法](# 方法)
- [C# 数组(Array)](# 数组(Array))
- [C# 字符串(String)](# 字符串(String))
-
- 字符Char
- string基础
- 常用API
- 字符串的不变性、驻留性
- 字符串的查找、比较
- 字符串连接的8种方式
- [字符串插值](#字符串插值)
- 高性能字符串实践
- [C# 结构体(Struct)](# 结构体(Struct))
-
- 定义结构体
- [类 vs 结构](#类 vs 结构)
- [C# 枚举(Enum)](# 枚举(Enum))
-
- [声明 *enum* 变量](#声明 enum 变量)
- [C# 类(Class)](# 类(Class))
- [C# 继承](# 继承)
- [C# 多态性](# 多态性)
- [C# 运算符重载 operator](# 运算符重载 operator)
- [C# 接口(Interface)](# 接口(Interface))
- [C# 预处理器指令](# 预处理器指令)
- [C# 正则表达式](# 正则表达式)
-
- 分组构造
- [Regex 类](#Regex 类)
- [C# 异常处理](# 异常处理)
- [C# 文件的输入与输出](# 文件的输入与输出)
-
- [FileStream 类](#FileStream 类)
- [C# 高级文件操作](# 高级文件操作)
- [C# 特性(Attribute)](# 特性(Attribute))
- [C# 反射(Reflection)](# 反射(Reflection))
- [C# 属性(Property)](# 属性(Property))
- [C# 索引器(Indexer)](# 索引器(Indexer))
- [C# 委托(Delegate)](# 委托(Delegate))
- [C# 事件(Event)](# 事件(Event))
-
- 声明事件(Event)
- 事件vs委托
- 事件有什么用处?
- 委托、事件与Observer设计模式
-
- Observer设计模式简介和实例
- [符合.Net Framework规范的委托和事件](#符合.Net Framework规范的委托和事件)
- [C# 集合](# 集合)
-
- [常用的 C# 集合类型](# 集合类型)
- 使用场景:
- [C# 泛型(Generic)](# 泛型(Generic))
- [C# 匿名方法](# 匿名方法)
- [C# 多线程](# 多线程)
C# 介绍
C#和.NET Framework
C#是一种通用的,类型安全的,面向对象的编程语言。
C#有许多功能,平衡简单性,表达性和性能。
C#语言是平台无关的,但与Microsoft .NET Framework非常相似。
对象定向
C#是面向对象范例的实现,包括封装,继承和多态。
统一型系统
C#有一个统一的类型系统,所有类型最终共享一个共同的基本类型。
类和接口
C#可以定义一个接口,类,事件,委托等等。
在C#中,方法只是一种函数成员,它还包括属性和事件。
属性是封装一个对象状态的函数成员,例如按钮的颜色或标签的文本或产品的价格。
事件是简化对对象状态更改的函数成员。 C#有一个正式的方式来创建事件。
类型安全
C#主要是一种类型安全的语言。
C#支持静态类型,意味着语言在编译时强制类型安全。
C#允许你的代码的部分动态dynamic
关键字。
内存管理
C#依赖运行时来执行自动内存管理。
公共语言运行时(CLR)有一个垃圾收集器,作为程序的一部分执行,为不再被引用的对象回收内存。
C#和CLR
C#通常用于编写在Windows平台上运行的代码。
C#的设计紧密地映射到Microsoft的通用语言运行时(CLR)的设计。
CLR提供这些运行时特性。
C#类型系统紧密地映射到CLR类型系统。
.NET Framework由CLR和一组库组成。
CLR是用于执行托管代码的运行时。
C# 封装
private | 私有成员, 在类的内部才可以访问(只能从其声明上下文中进行访问) |
---|---|
protected | 保护成员,该类内部和从该类派生的类中可以访问 |
Friend | 友元 ,声明 friend 元素的程序集中的代码可以访问该元素,而不能从程序集外部访问。 |
Protected Friend | 在派生类或同一程序集内都可以访问。 |
public | 公共成员,完全公开,没有访问限制。 |
internal | 在同一程序集中可以访问。(很少用) |
比如说:一个人A为父类,他的儿子B,妻子C,私生子D(注:D不在他家里)
如果我们给A的事情增加修饰符:
- public事件,地球人都知道,全公开
- protected事件,A,B,D知道(A和他的所有儿子知道,妻子C不知道)
- private事件,只有A知道(隐私?心事?)
- internal事件,A,B,C知道(A家里人都知道,私生子D不知道)
- protected internal事件,A,B,C,D都知道,其它人不知道
C# 方法
定义方法
当定义一个方法时,从根本上说是在声明它的结构的元素。在 C# 中,定义方法的语法如下:
c#
<Access Specifier> <Return Type> <Method Name>(Parameter List)
{
Method Body
}
下面是方法的各个元素:
- Access Specifier:访问修饰符,这个决定了变量或方法对于另一个类的可见性。
- Return type :返回类型,一个方法可以返回一个值。返回类型是方法返回的值的数据类型。如果方法不返回任何值,则返回类型为 void。
- Method name:方法名称,是一个唯一的标识符,且是大小写敏感的。它不能与类中声明的其他标识符相同。
- Parameter list:参数列表,使用圆括号括起来,该参数是用来传递和接收方法的数据。参数列表是指方法的参数类型、顺序和数量。参数是可选的,也就是说,一个方法可能不包含参数。
- Method body:方法主体,包含了完成任务所需的指令集。
调用方法:
c#
using System;
namespace FindMax
{
class FindMax
{
public int Fm(int num1, int num2)
{
int result;
if (num1 > num2)
{
result = num1;
}
else
{
result = num2;
}
return result;
}
static void Main(string[] args)
{
int a = 100;
int b = 200;
int ret;
FindMax n = new FindMax();
ret = n.Fm(a, b);
Console.WriteLine("最大值是:{0}", ret);
Console.ReadKey();
}
}
}
参数传递:
当调用带有参数的方法时,您需要向方法传递参数。在 C# 中,有三种向方法传递参数的方式:
方式 | 描述 |
---|---|
值参数 | 这种方式复制参数的实际值给函数的形式参数,实参和形参使用的是两个不同内存中的值。在这种情况下,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。 |
引用参数 | 这种方式复制参数的内存位置的引用给形式参数。这意味着,当形参的值发生改变时,同时也改变实参的值。 |
输出参数 | 这种方式可以返回多个值。 |
按值传递参数
按引用传递参数 ref
引用参数是一个对变量的内存位置的引用。当按引用传递参数时,与值参数不同的是,它不会为这些参数创建一个新的存储位置。引用参数表示与提供给方法的实际参数具有相同的内存位置。
在 C# 中,使用 ref 关键字声明引用参数。下面的实例演示了这点:
c
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication
{
class Parameters
{
public void swap(ref int x, ref int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
static void Main(string[] args)
{
Parameters n = new Parameters();
int a = 100;
int b = 200;
n.swap(ref a, ref b);
Console.WriteLine("a:{0}", a);
Console.WriteLine("b:{0}", b);
Console.ReadKey();
}
}
}
按输出传递参数 out 多返回值
return 语句可用于只从函数中返回一个值。但是,可以使用 输出参数 来从函数中返回两个值。输出参数会把方法输出的数据赋给自己
提供给输出参数的变量不需要赋值。当需要从一个参数没有指定初始值的方法中返回值时,输出参数特别有用。
c#
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void getValues(out int x, out int y )
{
Console.WriteLine("请输入第一个值: ");
x = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("请输入第二个值: ");
y = Convert.ToInt32(Console.ReadLine());
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a , b;
/* 调用函数来获取值 */
n.getValues(out a, out b);
Console.WriteLine("在方法调用之后,a 的值: {0}", a);
Console.WriteLine("在方法调用之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
C# 可空类型(Nullable)
单问号 ?
? 单问号用于对 int、double、bool 等无法直接赋值为 null 的数据类型进行 null 的赋值,意思是这个数据类型是 Nullable 类型的。
int? , 表示可空类型,即是值可以为null
c#
int? i = null;
double? j = null;
int? i与 int i的区别?
可以通过default(i)
获取默认值
c#
int i = default(int);//默认值为0
int? j = default(int?);//默认值为null
// null判断
int? k = null;
if (k == null){
///
}
双问号 ??
?? 双问号用于判断一个变量在为 null 的时候返回一个指定的值。
c#
double? num1 = null;
double num2 = 3.14157;
double num3 = num1 ?? 5.34; // num1 如果为空值则返回 5.34
Console.WriteLine("num3 的值: {0}", num3);
Console.ReadKey();
?? 可以理解为三元运算符的简化形式:
c#
num3 = num1 ?? 5.34;
num3 = (num1 == null) ? 5.34 : num1;
int?转int
cs
i = (int)j;
*null与任何值运算,结果还是null*
cs
j = null;
int? k = j + 5;//k值为n
C# 数组(Array)
数组是一个存储相同类型元素的固定大小的顺序集合。所有的数组都是由连续的内存位置组成的。最低的地址对应第一个元素,最高的地址对应最后一个元素。
声明数组
在 C# 中声明一个数组,您可以使用下面的语法:
datatype[] arrayName;
其中,
- datatype 用于指定被存储在数组中的元素的类型。
- [ ] 指定数组的秩(维度)。秩指定数组的大小。
- arrayName 指定数组的名称。
例如:
c#
double[] balance;
初始化数组
数组是一个引用类型,所以您需要使用 new 关键字来创建数组的实例。
c#
double[] balance = new double[10]; //已知长度
string[] fruits = new string[] { "apple", "orange", "banana" }; //已知元素
int[] nums = { 7, 8, 9 }; // 简版
数组长度
可以通过数组的Length
属性获取数组的长度。
c#
int[] numbers = { 10, 20, 30, 40, 50 };
int length = numbers.Length; // 获取数组的长度,此处为5
遍历数组
使用 for
循环遍历数组
c#
int[] numbers = { 1, 2, 3, 4, 5 };
// 使用 for 循环遍历数组
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(numbers[i]);
}
使用 foreach
循环遍历数组
c#
int[] numbers = { 1, 2, 3, 4, 5 };
// 使用 foreach 循环遍历数组
foreach (int number in numbers)
{
Console.WriteLine(number);
}
函数的数组参数
c#
double getAverage(int[] arr, int size){
// ...
}
static void Main(string[] args){
int [] balance = new int[]{1000, 2, 3, 17, 50};
/* 传递数组的指针作为参数 */
avg = app.getAverage(balance, 5 ) ;
// ...
}
参数数组(params)
有时,当声明一个方法时,您不能确定要传递给函数作为参数的参数数目。C# 参数数组解决了这个问题**,参数数组通常用于传递未知数量的参数给函数**。
params
关键字可以指定采用数目可变的参数的 参数方法。
- 1.既可以接受数组,也可以接受多个和形参同类型的值
- 2.不允许和 ref、out 同时使用;
- 3.带 params 关键字的参数必须是最后一个参数,并且在方法声明中只允许一个 params 关键字。
- 4.不能仅使用 params 来使用重载方法。
- 5.没有 params 关键字的方法的优先级高于带有params关键字的方法的优先级
c#
using System;
namespace MyParams
{
class MyParams
{
static void Test(int a, int b)
{
Console.WriteLine("a + b = {0}", a + b);
}
static void Test(params int[] list)
{
foreach (int i in list)
{
Console.Write("{0} ", i);
}
Console.WriteLine();
}
static void Main(string[] args)
{
Test(1, 2);
int[] a = new int[] { 1, 2, 3, 4, 5 };
Test(a); // 接受数组
Test(7, 8, 9);// 接受多个和形参同类型的值
Console.ReadLine();
}
}
}
多维数组
二维数组
c#
// 创建二维数组
int[,] matrix = new int[3, 3]
{
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
数组方法
排序
Array.Sort()
方法用于对数组元素进行排序。默认情况下,会按照数组元素的自然顺序进行升序排序。
c#
int[] numbers = { 5, 2, 8, 1, 9 };
Array.Sort(numbers); // 对数组进行升序排序 { 1, 2, 5, 8, 9 }
反转
Array.Reverse()
方法用于反转数组元素的顺序。
c#
int[] numbers = { 1, 2, 3, 4, 5 };
Array.Reverse(numbers); // 反转数组元素的顺序
查找索引
Array.IndexOf()
方法用于查找指定元素在数组中的索引。
c#
int[] numbers = { 10, 20, 30, 40, 50 };
int index = Array.IndexOf(numbers, 30); // 查找元素30的索引,此处为2
复制
Array.Copy()
方法用于复制数组的一部分到另一个数组中。
c#
int[] source = { 1, 2, 3, 4, 5 };
int[] destination = new int[3];
Array.Copy(source, 1, destination, 0, 3); // 从source数组的索引1开始复制3个元素到destination数组
C# 字符串(String)
字符Char
char 是值类型 (结构体),以16位整数形式存储,char
可隐式转换为int
。字面量用单引号''
包裹。
string基础
字符串 string 是一个不可变(不可修改)的字符序列(数组),为引用类型 ,字面量用双引号""
包裹。
- 字符串可以当做 字符数组一样操作,只是不能修改。
- 字符串的相等为值比较 ,只要字符序列相同即可。例外情况请是如果用
object
做==
比较,只会比较引用地址。
常用API
属性 | 特点/说明 |
---|---|
Length | 字符串中字符数量 |
[索引器int index] | 索引器,用索引获取字符,不可修改 |
🔸方法 | 特点/说明 |
StartsWith、EndsWith(String) | 判断开头、结尾是否匹配,"Hello".StartsWith("He") |
Equals(String) | 比较字符串是否相同 |
IndexOf() | 查找指定字符(串)的索引位置,从后往前查找 LastIndexOf |
Insert(Int32, String) | 指定位置插入字符串,‼️返回新字符串! |
PadLeft(Int32) | 指定字符宽度(数量)对齐,左侧填充,‼️返回新字符串!右侧填充 PadRight(Int32) |
Remove(Int32, Int32) | 删除指定位置、长度的字符,‼️返回新字符串! |
Replace(String, String) | 替换指定内容的字符(串),‼️返回新字符串! |
Substring(Int32, Int32) | 截取指定位置、长度的字符串,‼️返回新字符串! |
ToLower()、ToUpper() | 返回小写、大写形式的字符串,‼️返回新字符串! |
Trim() | 裁剪掉前后空格,‼️返回新字符串!有多个配套方法 TrimEnd、TrimStart |
Split(char) | 按分隔符分割字符串为多个子串,比较常用,不过性能不好,建议用Span代替。 |
🔸静态方法 | 特点/说明 |
Empty | 获取一个空字符串(同"" ) |
Compare(String, String) | 比较两个字符串,有很多重载,返回一个整数,0表示相同。 |
[Concat (params string?]) | 连接多个字符串,返回一个新的字符串,有很多重载,是比较基础的字符串连接函数。 |
Equals(str, StringComparison) | 比较字符串是否相同,可指定比较规则 StringComparison |
[Format (String, Object]) | 字符串格式化,远古时期常用的字符串格式化方式,现在多实用$插值 |
string Intern(String) | 获取"内部"字符串,先检查字符串池中是否存在,有则返回其引用,没有则添加并返回 |
string? IsInterned(String) | 判断是否在字符串池 中,存在则返回其引用,没有则返回null |
IsNullOrEmpty(String) | 判断指定的字符串是否 null 、空字符"" /String.Empty ,返回bool |
IsNullOrWhiteSpace(String) | 判断指定的字符串是否 null 、空字符"" /String.Empty 、空格字符,返回bool |
[Join (Char, String]) | 用分隔符连接一个数组为一个字符串 |
字符串的不变性、驻留性
字符串是一种有一点点特别的引用类型,因为其不变性,所以在参数传递时有点像值类型。
- **🔸不变性:**字符串一经创建,值不可变。对字符串的各种修改操作都会创建新的字符串对象,这一点要非常重视,应尽量避免,较少不必要的内存开销。
- 🔸驻留性:运行时将字符串值存储在"驻留池(字符串池)"中,相同值的字符串都复用同一地址。
当然不是所有字符串都会驻留,那样驻留池不就撑爆了吗!一般只有两种情况下字符串会被驻留:
- 字面量的字符串,这在编译阶段就能确定的"字符串常量值"。相同值的字符串只会分配一次,后面的就会复用同一引用。
- 通过
string.Intern(string)
方法主动添加驻留池。
驻留的字符串(字符串池)在托管堆上存储,大家共享,内部其实是一个哈希表,存储被驻留的字符串和其内存地址。驻留池生命周期同进程,并不受GC管理,因此无法被回收。因此需要注意:
lock
锁不能用string,避免使用同一个锁(字符串引用)。- 避免创建字面量的大字符串,会常住内存无法释放,当然也不要滥用
string.Intern(string)
方法。
字符串的查找、比较
string 的 比较字符串 是默认包含文化和区分大小写的顺序比较,C#内置的一个字符串比较规则(枚举)StringComparison,可设置比较规则。在很多内置方法中使用,包括 String.Equals、String.Compare、String.IndexOf 和 String.StartsWith等。
字符串连接的8种方式
连接方法 | 示例/说明 |
---|---|
直接相加 | "hello"+str ,其实编译后为 string.Concat ("hello", str) |
连接函数:String.Concat() | 字符串相加一般就是被编译为调用String.Concat() 方法,有很多重载,支持任意多个参数 |
集合连接函数:String.Join() | 将(集合)参数连接为一个字符串,string.Join('-',1,2,3); //1-2-3 |
格式化:String.Format() | 传统的字符串格式化手艺,string.Format("name:{0},age:{1}",str,18) |
$ 字符串插值 | 用花括号{var} 引用变量、表达式,强大、方便,$"Hello {name} !" |
@ 逐字文本字面量 |
支持转义符号、换行符,常用于文件路径、多行字符:@$"C:\\Users\\{name}\\Downloads" |
""" 原始字符串字面量 |
C# 11,三个双冒号包围,支持多行文本的原始字面量。 |
StringBuilder |
当处理大量字符串连接操作时,推荐使用StringBuilder ,效果更优。 |
$字符串插值
字符串插值的格式:$"{<interpolationExpression>}"
,大括号中可以是一个变量,一个(简单)表达式语句,还支持设置格式。功能强大、使用方便,老人孩子都爱用!
{}
字符转义,用两个{``{}}
即可,如果只有一边,则用单引号'{``{'
,即输出为{
。- 使用三元运算符
?
表达式,用括号包起来即可,因为":
"在插值字符串中有特殊含义,即格式化。 - 字符串格式规则参考后文《字符串格式总结》。
c#
var name = "sam";
Console.WriteLine($"Hello {name}!"); //Hello sam!
Console.WriteLine($"日期:{DateTime.Now.AddDays(1):yyyy-MM-dd HH:mm:ss}"); //日期:2024-01-18 23:21:55!
Console.WriteLine($"ThreadID:{Environment.CurrentManagedThreadId:0000}"); //ThreadID:0001
Console.WriteLine($"Length:{name.Length}"); //Length:3
Console.WriteLine($"Length:{(name.Length>3?"OK":"Error")}"); //Length:Error
@
标记的字符串为字面量字符串 ,不需要使用转义字符了,可搭配$
字符串插值使用。文件路径地址都会用到@
,两个冒号表示一个冒号,@"a""b"
==a"b
。
c#
var path= @"D:\GApp\LINQPad 8\x64";
var file = $@"D:\GApp\LINQPad 8\x64\{DateTime.Now:D}";
var maxText = @"Hi All:
第一行
换行
";
高性能字符串实践
提高string处理性能的核心就是:尽量减少临时字符串对象的创建。
- 高频常用字符串(非字面量)可考虑主动驻留字符串,
string.Intern(name)
。 - 字符串的比较、查找,优先用Span,或者尽量使用无文化语义的比较
StringComparison.Ordinal
。 - 大量字符串连接使用StringBuilder,且尽量给定一个合适的容量大小,避免频繁的扩容。
- 少量字符串连接用字符串插值即可,创建StringBuilder也是有成本的。
- 如果有大量StringBuilder 的使用,可以考虑用StringBuilderCache,或池化StringBuilder。
C# 结构体(Struct)
在 C# 中,结构体(struct)是一种值类型(value type),用于组织和存储相关数据。
定义结构体
c#
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};
类 vs 结构
类适合表示复杂的对象和行为,支持继承和多态性,而结构则更适合表示轻量级数据和值类型,以提高性能并避免引用的管理开销。
首先明确,类的对象是存储在堆空间中,结构存储在栈中。堆空间大,但访问速度较慢,栈空间小,访问速度相对更快。故而,当我们描述一个轻量级对象的时候,结构可提高效率,成本更低。当然,这也得从需求出发,假如我们在传值的时候希望传递的是对象的引用地址而不是对象的拷贝,就应该使用类了。
值类型 vs 引用类型:
- 结构是值类型(Value Type): 结构是值类型,它们在栈上分配内存,而不是在堆上。当将结构实例传递给方法或赋值给另一个变量时,将复制整个结构的内容。
- 类是引用类型(Reference Type): 类是引用类型,它们在堆上分配内存。当将类实例传递给方法或赋值给另一个变量时,实际上是传递引用(内存地址)而不是整个对象的副本。
继承和多态性:
- 结构不能继承: 结构不能继承其他结构或类,也不能作为其他结构或类的基类。
- 类支持继承: 类支持继承和多态性,可以通过派生新类来扩展现有类的功能。
默认构造函数:
- 结构不能有无参数的构造函数: 结构不能包含无参数的构造函数。每个结构都必须有至少一个有参数的构造函数。
- 类可以有无参数的构造函数: 类可以包含无参数的构造函数,如果没有提供构造函数,系统会提供默认的无参数构造函数。
赋值行为:
- 类型为类的变量在赋值时存储的是引用,因此两个变量指向同一个对象。
- 结构变量在赋值时会复制整个结构,因此每个变量都有自己的独立副本。
- 结构体中声明的字段无法赋予初值,类可以:
传递方式:
- 类型为类的对象在方法调用时通过引用传递,这意味着在方法中对对象所做的更改会影响到原始对象。
- 结构对象通常通过值传递,这意味着传递的是结构的副本,而不是原始结构对象本身。因此,在方法中对结构所做的更改不会影响到原始对象。
可空性:
- **结构体是值类型,不能直接设置为 *null* :**因为 null 是引用类型的默认值,而不是值类型的默认值。如果你需要表示结构体变量的缺失或无效状态,可以使用 Nullable 或称为 T? 的可空类型。
- 类默认可为null: 类的实例默认可以为
null
,因为它们是引用类型。
性能和内存分配:
- 结构通常更轻量: 由于结构是值类型且在栈上分配内存,它们通常比类更轻量,适用于简单的数据表示。
- 类可能有更多开销: 由于类是引用类型,可能涉及更多的内存开销和管理。
C# 枚举(Enum)
枚举是一组命名整型常量。枚举类型是使用 enum 关键字声明的。
C# 枚举是值类型。换句话说,枚举包含自己的值,且不能继承或传递继承
声明 enum 变量
枚举列表中的每个符号代表一个整数值,一个比它前面的符号大的整数值。默认情况下,第一个枚举符号的值是 0.例如:
c#
enum Days { Sun, Mon, tue, Wed, thu, Fri, Sat };
- 枚举是一种类型
- 适用于某些取值范围有限的数据,例如:我只想要春夏秋冬这四个字符的取值范围,如果用string的话就无法固定取值范围
- 枚举语法:[访问权限修饰符] enum 枚举名 { 枚举值 }
- 枚举访问权限修饰符和类是一样的,默认访问权限和类一样都是internal
- 枚举名遵循大驼峰命名法
- 枚举的每一个值都是一个整型,默认都是从 0 开始
c#
using System;
public class EnumTest
{
enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
static void Main()
{
int x = (int)Day.Sun;
int y = (int)Day.Fri;
Console.WriteLine("Sun = {0}", x);
Console.WriteLine("Fri = {0}", y);
}
}
C# 类(Class)
类 (class) 是最基础的 C# 类型。类是一个数据结构,将状态(字段)和操作(方法和其他函数成员)组合在一个单元中。类为动态创建的类实例提供了定义,实例也称为对象。类支持继承和多态性,这是派生类 可用来扩展和专用化基类的机制。
类的定义
类的定义是以关键字 class 开始,后跟类的名称。类的主体,包含在一对花括号内
可访问性
类的每个成员都有关联的可访问性,它控制能够访问该成员的程序文本区域。有五种可能的可访问性形式。下表概述了这些可访问性。
可访问性 | 含义 |
---|---|
public | 访问不受限制 |
protected | 访问仅限于此类和从此类派生的类 |
internal | 访问仅限于此程序 |
protected internal | 访问仅限于此程序和从此类派生的类 |
private | 访问仅限于此类 |
如果没有指定,则使用默认的访问标识符。类的默认访问标识符是 internal ,成员的默认访问标识符是 private。
构造函数
类的 构造函数 是类的一个特殊的成员函数,当创建类的新对象时执行。
构造函数的名称与类的名称完全相同,它没有任何返回类型。
默认的构造函数 没有任何参数。但是如果你需要一个带有参数的构造函数可以有参数,这种构造函数叫做参数化构造函数。这种技术可以帮助你在创建对象的同时给对象赋初始值
c#
using System;
namespace ConsoleApplication
{
class ClassTest
{
private double length;
public ClassTest() // 构造函数
{
Console.WriteLine("对象已创建");
}
public ClassTest(double len) // 构造函数
{
Console.WriteLine("对象已创建,length = {0}", len);
length = len;
}
public void setLength(double len)
{
length = len;
}
public double getLength()
{
return length;
}
static void Main()
{
ClassTest cs1 = new ClassTest();
cs1.setLength(6.0);
Console.WriteLine("cs1线条的长度为:{0}", cs1.getLength());
ClassTest cs2 = new ClassTest(7.0);
Console.WriteLine("cs2线条的长度为:{0}", cs2.getLength());
Console.ReadKey();
}
}
}
析构函数
类的 析构函数 是类的一个特殊的成员函数,当类的对象超出范围时执行。
析构函数的名称是在类的名称前加上一个波浪形(~)作为前缀,它不返回值,也不带任何参数。
析构函数用于在结束程序(比如关闭文件、释放内存等)之前释放资源。析构函数不能继承或重载。
c#
~ClassTest()// 析构函数
{
Console.WriteLine("对象已删除");
}
静态成员
我们可以使用 static 关键字把类成员定义为静态的。当我们声明一个类成员为静态时,意味着无论有多少个类的对象被创建,只会有一个该静态成员的副本。
关键字 static 意味着类中只有一个该成员的实例。静态变量用于定义常量,因为它们的值可以通过直接调用类而不需要创建类的实例来获取。静态变量可在成员函数或类的定义外部进行初始化。你也可以在类的定义内部初始化静态变量。
c#
using System;
namespace ConsoleApplication
{
class StaticVar{
public static int num;
public void count()
{
num++;
}
public int getNum(){
return num;
}
}
class StaticTest
{
static void Main()
{
StaticVar s1 = new StaticVar();
StaticVar s2 = new StaticVar();
s1.count();
s1.count();
s1.count();
Console.WriteLine("s1 的变量 num: {0}", s1.getNum());
Console.WriteLine("s2 的变量 num: {0}", s2.getNum());
s2.count();
s2.count();
s2.count();
Console.WriteLine("s1 的变量 num: {0}", s1.getNum());
Console.WriteLine("s2 的变量 num: {0}", s2.getNum());
Console.ReadKey();
}
}
}
静态函数
你也可以把一个成员函数 声明为 static 。这样的函数只能访问静态变量。静态函数在对象被创建之前就已经存在。
c#
using System;
namespace ConsoleApplication
{
class StaticVar{
public static int num;
public void count()
{
num++;
}
public static int getNum(){ // 静态函数
return num;
}
}
class StaticTest
{
static void Main()
{
StaticVar s = new StaticVar();
s.count();
s.count();
s.count();
Console.WriteLine("s1 的变量 num: {0}", StaticVar.getNum());
Console.ReadKey();
}
}
}
C# 继承
当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。这个已有的类被称为的基类(父类) ,这个新的类被称为派生类(子类)。
- 1、继承的语法:class 子类类名 : class 父类类名{ //子类类体 }
- 2、继承的特点:子类拥有所有父类中所有的字段、属性和方法
- 3、一个类可以有多个子类,但是父类只能有一个
- 4、一个类在继承另一个类的同时,还可以被其他类继承
- 5、在 C# 中,所有的类都直接或者间接的继承自 Object 类
继承的声明
c#
class BaseClass
{
public void SomeMethod()
{
// Method implementation
}
}
class DerivedClass : BaseClass
{
public void AnotherMethod()
{
// Accessing base class method
base.SomeMethod();
// Method implementation
}
}
c#
using System;
namespace ConsoleApplication
{
class Shape
{
protected int width; // 访问仅限于此类和从此类派生的类
protected int height; // 访问仅限于此类和从此类派生的类
public void setWidth(int w)
{
width = w;
}
public void setHeight(int h)
{
height = h;
}
}
class Rectangle : Shape
{
public int getArea()
{
return (width * height);
}
}
class ClassInherit
{
static void Main()
{
Rectangle rect = new Rectangle();
rect.setWidth(5);
rect.setHeight(7);
Console.WriteLine("总面积:{0}", rect.getArea());
Console.ReadKey();
}
}
}
一个接口可以继承自一个或多个其他接口,派生接口继承了基接口的所有成员。
派生接口可以扩展基接口的成员列表,但不能改变它们的访问修饰符。
继承接口
一个接口可以继承自一个或多个其他接口,派生接口继承了基接口的所有成员。
派生接口可以扩展基接口的成员列表,但不能改变它们的访问修饰符。
c#
interface IBaseInterface
{
void Method1();
}
interface IDerivedInterface : IBaseInterface
{
void Method2();
}
C# 多态性
多态是同一个行为具有多个不同表现形式或形态的能力。
多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。
现实中,比如我们按下 F1 键这个动作:
- 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
- 如果当前在 Word 下弹出的就是 Word 帮助;
- 在 Windows 下弹出的就是 Windows 帮助和支持。
同一个事件发生在不同的对象上会产生不同的结果。
静态多态性 - 重载
重载(overload): 在同一个作用域(一般指一个类)的两个或多个方法函数名相同,参数列表不同的方法叫做重载,它们有三个特点(俗称两必须一可以):
- 方法名必须相同
- 参数列表必须不相同
- 返回值类型可以不相同
函数重载
c#
using System;
namespace PolymorphismApplication
{
public class TestData
{
public int Add(int a, int b, int c)
{
return a + b + c;
}
public int Add(int a, int b)
{
return a + b;
}
}
class Program
{
static void Main(string[] args)
{
TestData dataClass = new TestData();
int add1 = dataClass.Add(1, 2);
int add2 = dataClass.Add(1, 2, 3);
Console.WriteLine("add1 :" + add1);
Console.WriteLine("add2 :" + add2);
}
}
}
运算符重载
动态多态性 - 重写
重写(override):子类中为满足自己的需要来重复定义某个方法的不同实现,需要用 override 关键字,被重写的方法必须是虚方法,用的是 virtual 关键字。它的特点是(三个相同):
- 相同的方法名
- 相同的参数列表
- 相同的返回值
C#中子类重写父类方法的几种情况,重写的方法必须是 virtual、abstract 或 override 修饰的 ,不能重写非虚方法或静态方法。关键字:virtual、abstract、override、new
修饰父类 - 声明:
虚函数 virtual
virtualvirtual
:不是必须被子类重写的方法,父类必须给出实现,子类可以重写(使用override,new,或无特殊标志的普通方法),也可以不重写该方法。
🔸virtual关键字用于修饰方法、属性、索引器或事件声明 ,并使它们可以在派生类中被重写。
🔸虚拟成员的实现可由派生类中的重写成员更改。默认情况下,方法是非虚拟的。不能重写非虚方法。
🔸virtual 修饰符不能与 static、abstract, private 或 override 修饰符一起使用。
🔸重写的父类方法时,必须指明被重写的方法是虚方法(virtual 关键字)。在子类重写父类方法时必须有重写关键字(override)这样才能重写父类的方法实例:
实例:
c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication
{
public class Person
{
public virtual void Show()
{
Console.WriteLine("我是人类");
}
}
public class Student : Person
{
public override void Show()
{
Console.WriteLine("我是学生");
}
}
class virtualTest
{
static void Main()
{
Student stu = new Student();
stu.Show();
Console.ReadKey();
}
}
}
抽象 abstract
abstract
修饰类名为抽象类,修饰方法为抽象方法。
🔸如果一个类为抽象类,则这个类只能其他某个类的基类。
🔸抽象方法在抽象类中没有函数体。抽象类中的抽象方法是没有方法体的,继承其的子类必须实现抽象类的抽象方法。
抽象类:
抽象类就是指设计为被继承的类。抽象类只能用作其他类的基类。
- 抽象类不能实例化
- 抽象类的派生类必须实现所有抽象方法
- 抽象类中的抽象方法是没有方法体的,继承其的子类必须实现抽象类的抽象方法
- 通过在类定义前面放置关键字 sealed ,可以将类声明为密封类 。当一个类被声明为 sealed 时,它不能被继承。抽象类不能被声明为 sealed。
抽象方法:
- 抽象方法是隐式的虚方法
- 只允许在抽象类中使用抽象方法声明
- 抽象方法在抽象类中没有方法体
- 在抽象方法声明中,不能使用static或者virtual修饰符
- 抽象方法只能再抽象类中声明。
c#
using System;
namespace abstractTest
{
public abstract class Person
{
public abstract void Show();
public void Shout(){ // 抽象类可以包含非抽象函数(普通方法)
Console.WriteLine("Hello");
}
abstract public int Age { get; set; } //抽象属性
}
public class Student : Person
{
public override void Show()
{
Console.WriteLine("我是学生");
}
private int _Age;
public override int Age //重写抽象属性
{
get
{
return _Age;
}
set
{
_Age = value;
}
}
}
class abstractTest
{
static void Main()
{
Student stu = new Student();
stu.Show();
Console.ReadKey();
}
}
}
使用场景:
🔸如果一个类设计的目点是用来被其它类继承的,它代表一类对象的所具有的公共属性或方法,那个这个类就应该设置为抽象类。我觉得用简单的话来说抽象类的功能就是:我是老子(抽象类),你要是跟了(继承)老子,你就必须得会干什么(实际实现)
🔸举个场景就是:老子会打人,那你也必须会打人,但你是轻轻的打,还是狠狠的打,你自己决定,但你必须得会打人。
如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧
总结:
父类有abstract的方法里,子类一定要对该方法override,而virtual的方法,可以不用.
修饰子类 - 实现:
override
override 方法提供从基类继承的成员的新实现。由 override 声明重写的方法称为重写基方法。
🔸重写的基方法必须与 override 方法具有相同的签名。
🔸override 声明不能更改 virtual 方法的可访问性。 override 方法和 virtual 方法必须具有相同的访问级别修饰符。
new
c#中,new的关键字主要有三个功能:
- 作为运算符用来创建类的一个对象。
Class obj = new Class();
- 作为修饰符。
- 用于在泛型声明中约束可能用作类型参数的参数类型。(这个不太清楚)
在这里主要介绍第2个功能,作为修饰符的作用。
C# new关键字表示隐藏,是指加上new关键字的属性或函数将对本类和继承类隐藏基类的同名属性或函数
总结:
父类引用指向子类对象时调用方法:
c#
// A是B的父类,此时就是父类引用指向子类对象
A a = new B();
🔸如果用是override重写,执行的是子类的方法;
🔸如果用的是new覆盖,执行的是父类的方法。
c#
using System;
namespace overrideTest
{
public class Person
{
public virtual void Show()
{
Console.WriteLine("我是人类");
}
}
public class Student : Person
{
public override void Show()
{
Console.WriteLine("我是学生");
}
}
public class Teacher : Person
{
public new void Show()
{
Console.WriteLine("我是老师");
}
}
class overrideTest
{
static void Main()
{
Person stu = new Student();
stu.Show();
Person tea = new Teacher();
tea.Show();
Console.ReadKey();
}
}
}
C# 运算符重载 operator
可以重定义或重载 C# 中内置的运算符。因此,程序员也可以使用用户自定义类型的运算符。重载运算符是具有特殊名称的函数,是通过关键字 operator 后跟运算符的符号来定义的。
实现
c#
using System;
namespace operatorTest
{
class Box
{
private double length ;
private double width;
private double height;
public Box()
{
length = 0;
width = 0;
height = 0;
}
public Box(double len, double wid, double hei)
{
length = len;
width = wid;
height = hei;
}
public double getVolume() // 获取体积
{
return length * width * height;
}
public static Box operator +(Box a, Box b)
{
Box box = new Box();
box.length = a.length + b.length;
box.width = a.width + b.width;
box.height = a.height + b.height;
return box;
}
}
class operatorTest
{
static void Main()
{
Box b1 = new Box(6.0, 7.0, 5.0);
Box b2 = new Box(12.0, 13.0, 10.0);
Console.WriteLine("Box1 的体积: {0}", b1.getVolume());
Console.WriteLine("Box2 的体积: {0}", b2.getVolume());
Box b3 = b1 + b2;
Console.WriteLine("Box3 的体积: {0}", b3.getVolume());
Console.ReadKey();
}
}
}
C# 接口(Interface)
在 C# 语言中,类之间的继承关系仅支持单重继承,而接口是为了实现多重继承关系设计的。一个类能同时实现多个接口,还能在实现接口的同时再继承其他类,并且接口之间也可以继承。
🚩 接口是一种定义了一组方法、成员属性和事件的合约。通过使用接口,可以实现类中间的松耦合,使代码更加灵活和可扩展。
作用:
- 实现多态:接口允许一个类实现多个接口,从而可以在同一个类中实现不同接口的方法。这使得对象可以根据调用时的上下文执行不同的操作,提高了代码的灵活性和可复用性。
- 定义合约:接口为类之间的交互提供了一种规范。通过定义接口,我们可以明确指定类应该提供哪些方法和属性,并且其他类可以按照接口的规范进行开发,确保类之间的协作正常进行。
- **实现接口隔离原则:**接口隔离原则规定一个类应该对其他类有最小限度的依赖性。通过将功能拆分成多个接口,每个接口都只包含一个特定的职责,我们可以避免类与过多的其他类产生依赖关系,从而降低了耦合度。
定义:
接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。接口定义了语法合同 "是什么" 部分,派生类定义了语法合同 "怎么做" 部分。
c#
interface 接口名称
{
//接口成员;
}
// 实现
class 类名 : 接口名
{
//类中的成员以及实现接口中的成员
}
🔸接口命名通常以 I 字母开头,例如Itest。
🔸口成员,不允许使用 public、private、protected、internal 访问修饰符
🔸不允许使用 static、virtual、abstract、sealed 修饰符。不能定义字段
🔸定义的方法不能包含方法体。
🔸必须实现接口的所有方法。
实现:
接口的实现与类的继承语法格式类似,也是重写了接口中的方法,让其有了具体的实现内容。
c#
using System;
namespace interfaceTest
{
interface IBook
{
int Id { get; set; }
string Name { get;set; }
double Price { get; set; }
double SalePrice(int discount); // 方法
}
class Book : IBook
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public double SalePrice(int discount)
{
double salePrice = Price * discount * 0.1;
return salePrice;
}
}
class interfaceTest
{
static void Main()
{
Book book = new Book();
book.Id = 1001;
book.Name = "tyname";
book.Price = 60;
Console.WriteLine("id:{0}", book.Id);
Console.WriteLine("书名:{0}", book.Name);
Console.WriteLine("定价:{0}", book.Price);
Console.WriteLine("售卖价:{0}", book.SalePrice(8));
Console.ReadKey();
}
}
}
接口(interface)和抽象类(abstract class)的区别
- 1、接口支持多继承,抽象类不能实现多继承。
- 2、接口只能定义抽象规则,抽象类既可以定义规则,还可能提供已实现的成员。
- 3、接口是一组行为规范,抽象类是一个不完全的类,着重族的概念。
- 4、接口可以用于支持回调,抽象类不能实现回调,因为继承不支持。
- 5、接口只包含方法、属性、索引器、事件的签名,但不能定义字段和包含实现的方法,抽象类可以定义字段、属性、包含有实现的方法。
- 6、接口可以作用于值类型和引用类型,抽象类只能作用于引用类型。例如,Struct就可以继承接口,而不能继承类。
C# 预处理器指令
使用预处理器指令的注意事项
- 提高代码可读性 :使用
#region
可以帮助分隔代码块,提高代码的组织性。 - 条件编译 :通过
#if
等指令可以在开发和生产环境中编译不同的代码,方便调试和发布。 - 警告和错误 :通过
#warning
和#error
可以在编译时提示开发人员注意特定问题。
指令 | 描述 |
---|---|
#define |
定义一个符号,可以用于条件编译。 |
#undef |
取消定义一个符号。 |
#if |
开始一个条件编译块,如果符号被定义则包含代码块。 |
#elif |
如果前面的 #if 或 #elif 条件不满足,且当前条件满足,则包含代码块。 |
#else |
如果前面的 #if 或 #elif 条件不满足,则包含代码块。 |
#endif |
结束一个条件编译块。 |
#warning |
生成编译器警告信息。 |
#error |
生成编译器错误信息。 |
#region |
标记一段代码区域,可以在IDE中折叠和展开这段代码,便于代码的组织和阅读。 |
#endregion |
结束一个代码区域。 |
#line |
更改编译器输出中的行号和文件名,可以用于调试或生成工具的代码。 |
#pragma |
用于给编译器发送特殊指令,例如禁用或恢复特定的警告。 |
#nullable |
控制可空性上下文和注释,允许启用或禁用对可空引用类型的编译器检查。 |
C# 正则表达式
需要引入:
c#
using System.Text.RegularExpressions;
分组构造
匹配以 "19" 开头的两位数字年份
c#
using System;
using System.Text.RegularExpressions;
namespace ConsoleApplication
{
class RegExApplication
{
public static void Main()
{
string input = "1851 1999 1950 1905 2003";
string pattern = @"(?<=19)\d{2}\b";
foreach (Match match in Regex.Matches(input, pattern))
Console.WriteLine(match.Value);
Console.ReadKey();
}
}
}
(?<=19)
:- 这是一个正向肯定预查(Positive Lookbehind)。
- 它确保匹配的字符串前面紧跟着 "19",但 "19" 不会成为整个匹配的一部分。
\d{2}
:- 这部分匹配两个数字字符(0-9)。
\b
:- 这个是单词边界(Word Boundary),它确保匹配的数字结束于一个单词边界。
结合起来,这个正则表达式的意思是:
- 确保有 "19" 出现在你要匹配的数字之前,
- 然后匹配两个数字字符,
- 并确保这些数字之后是一个单词边界(即,下一个字符不是字母或数字,或者匹配到字符串的结尾)。
换句话说,@"(?<=19)\d{2}\b"
匹配的是 "19" 后面的两个数字,并且这两个数字形成一个完整的单词或独立的数字片段。
@
符号用于声明逐字字符串文本 (Verbatim String Literal)。逐字字符串文本的主要作用是使字符串中的特殊字符(如反斜杠 \
)
Regex 类
匹配了以 'S' 开头的单词:
c#
using System;
using System.Text.RegularExpressions;
namespace ConsoleApplication
{
class RegExApplication
{
public static void showMatch(string text, string expr)
{
Console.WriteLine("The Expression:" + expr);
MatchCollection mc = Regex.Matches(text, expr);
foreach (Match m in mc){
Console.WriteLine(m);
}
}
public static void Main()
{
string str = "A Thousand Splendid Suns";
Console.WriteLine("Matching words that start with 'S': ");
showMatch(str, @"\bS\S*");
Console.ReadKey();
}
}
}
C# 异常处理
异常提供了一种把程序控制权从某个部分转移到另一个部分的方式。C# 异常处理时建立在四个关键词之上的:try 、catch 、finally 和 throw。
- try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。
- catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。
- finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。例如,如果您打开一个文件,不管是否出现异常文件都要被关闭。
- throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。
异常类
System.ApplicationException 和 System.SystemException 类是派生于 System.Exception 类的异常类。
System.ApplicationException 类支持由应用程序生成的异常。所以程序员定义的异常都应派生自该类。
System.SystemException 类是所有预定义的系统异常的基类。
异常类 | 描述 |
---|---|
System.IO.IOException | 处理 I/O 错误。 |
System.IndexOutOfRangeException | 处理当方法指向超出范围的数组索引时生成的错误。 |
System.ArrayTypeMismatchException | 处理当数组类型不匹配时生成的错误。 |
System.NullReferenceException | 处理当依从一个空对象时生成的错误。 |
System.DivideByZeroException | 处理当除以零时生成的错误。 |
System.InvalidCastException | 处理在类型转换期间生成的错误。 |
System.OutOfMemoryException | 处理空闲内存不足生成的错误。 |
System.StackOverflowException | 处理栈溢出生成的错误。 |
异常处理
下面是一个当除以零时抛出异常的实例:
c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication
{
class Exception
{
int result;
Exception()
{
result = 0;
}
public void division(int num1,int num2){
try{
result = num1 / num2;
}
catch(DivideByZeroException e){
Console.WriteLine("Exception caught: {0}", e);
}
finally
{
Console.WriteLine("Result: {0}", result);
}
}
static void Main(string[] args)
{
Exception d = new Exception();
d.division(25, 0);
Console.ReadKey();
}
}
}
创建用户自定义异常
C# 文件的输入与输出
一个 文件 是一个存储在磁盘中带有指定名称和目录路径的数据集合。当打开文件进行读写时,它变成一个 流。
从根本上说,流是通过通信路径传递的字节序列。有两个主要的流:输入流 和 输出流 。输入流 用于从文件读取数据(读操作),输出流用于向文件写入数据(写操作)。
FileStream 类
System.IO 命名空间中的 FileStream 类有助于文件的读写与关闭。该类派生自抽象类 Stream。
您需要创建一个 FileStream 对象来创建一个新的文件,或打开一个已有的文件。创建 FileStream 对象的语法如下:
c#
FileStream <object_name> = new FileStream( <file_name>,
<FileMode Enumerator>, <FileAccess Enumerator>, <FileShare Enumerator>);
例如,创建一个 FileStream 对象 F 来读取名为 sample.txt 的文件:
c#
FileStream F = new FileStream("sample.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
c#
using System;
using System.IO;
namespace FileIOApplication
{
class Program
{
static void Main(string[] args)
{
FileStream F = new FileStream("test.dat",
FileMode.OpenOrCreate, FileAccess.ReadWrite);
for (int i = 1; i <= 20; i++)
{
F.WriteByte((byte)i);
}
F.Position = 0;
for (int i = 0; i <= 20; i++)
{
Console.Write(F.ReadByte() + " ");
}
F.Close();
Console.ReadKey();
}
}
}
C# 高级文件操作
上面的实例演示了 C# 中简单的文件操作。但是,要充分利用 C# System.IO 类的强大功能,您需要知道这些类常用的属性和方法。
在下面的章节中,我们将讨论这些类和它们执行的操作。请单击链接详细了解各个部分的知识:
主题 | 描述 |
---|---|
文本文件的读写 | 它涉及到文本文件的读写。StreamReader 和 StreamWriter 类有助于完成文本文件的读写。 |
二进制文件的读写 | 它涉及到二进制文件的读写。BinaryReader 和 BinaryWriter 类有助于完成二进制文件的读写。 |
Windows 文件系统的操作 | 它让 C# 程序员能够浏览并定位 Windows 文件和目录。 |
C# 特性(Attribute)
C# 反射(Reflection)
先来看下我们写的代码究竟是如何被计算机执行的
对于计算机来讲,它只认识01010101之类的二进制代码,人类写的高级语言(如C#、JAVA等)计算机是没法识别的,所以需要将高级语言转化为01让计算机可以识别的二进制编码,中间是有一个过程的。就拿C#来讲,VS编译器会将编写好的代码进行编译,编译后会生成exe/dll文件,.Net Core里面已经不生成exe了,都是dll。dll和exe还需要CLR/JIT的即时编译成字节码,才能最终被计算机执行。有伙伴就会问为什么要编译2次呢,先编译到dll,再编译到字节码01呢,为什么不能一次性编译成字节码呢?因为我们写的是C#语言,但是真实运行的机器有很多种,可能是32位,也可能是64位,操作系统可能是windows、linux、unix等,不同的计算机不同的操作系统识别字节码的可能是不一样的,但是从高级语言编译成exe/dll这一步是一样的。所以只要在不同运行环境的计算机上安装对应的不同的CLR/JIT,就可以运行我们同一个exe/dll了。这里就大概讲下这样一个过程,后面会有章节详细讲解程序如何被计算机执行的。现在我们先关注编译生成的exe/dll,它包含2部分,分别是中间语言IL和源数据元数据metadata。IL里面包含我们写的大量的代码,比如说方法、实体类等。元数据metadata不是我们写的代码,它是编译器在编译的时候生成的描述,它可能是把命名空间、类名、属性名记录了一下,包括特性。
讲上面程序的编译过程跟反射有什么关系呢?我们反射就是读取metadata里面的数据的,然后去使用它。
C# 属性(Property)
【字段】
字段(Field)是一种表示与对象或类关联的变量的成员,字段声明用于引入一个或多个给定类型的字段。字段是类内部用的,private类型的变量(字段),通常字段写法都是加个"_"符号,然后声明只读属性,字段用来储存数据。
【属性】
属性(Property)是另一种类型的类成员,定义属性的目的是在于便于一些私有字段的访问。类提供给外部调用时用的可以设置或读取一个值,属性则是对字段的封装,将字段和访问自己字段的方法组合在一起,提供灵活的机制来读取、编写或计算私有字段的值。属性有自己的名称,并且包含get 访问器和set 访问器。
声明格式:
c#
属性修饰符 类型 属性名
{
get{
//get访问器代码
}
set{
//set访问器代码
}
}
属性分类:根据get访问器和set访问器是否存在,属性可按下列规则分类。
为什么需要引入属性?
那么问题来了,既然已经有字段用来存储数据,为什么还要引入属性来对数据进行访问,把声明的字段直接定义成公有的不就可以了吗?
解答:在C#中,我们可以非常自由的、毫无限制的访问公有字段,但在一些场合中,我们可能希望限制只能给字段赋于某个范围的值、或是要求字段只能读或只能写,或是在改变字段时能改变对象的其他一些状态,这些单靠字段是无法做到的,于是就有了属性。
一则是隐藏组件或类内部的真实成员;
二是用来建立约束的,比如,实现"有我没你"这种约束;
三是用来响应属性变化事件,当属性变化是做某事,只要写在set方法里就行。
🚩 用了get和set能让赋值和取值增加限制
c#
class Bank
{
private int money;//私有字段
public int Money //属性
{
//GET访问器,可以理解成另类的方法,返回已经被赋了值的私有变量 money
get { return money; }
//SET访问器,将我们打入的值赋给私有变量 money,并且加了限制,不能存负的
set
{
if (value >= 0)
{
money = value;
}
else
{
money = 0;
}
}
}
}
C#通过属性特性读取和写入字段(成员变量),而不直接直接读取和写入,以此来提供对类中字段的保护
【属性VS字段】
【适用情况】
字段:
🔸允许自由读写
🔸取值范围只收数据类型约束而无其他任何特定限制;
🔸值的变动不需要引发类中其它任何成员的相应变化;
如果均满足上述条件,那么我们便可以大胆地使用公共字段;
属性:
🔸 要求字段只能读或只能写;
🔸 需要限定字段的取值范围;
🔸 在改变一个字段的值的时候希望改变对象的其它一些状态;
如果满足上述条件中的任何一个,就应该使用属性。
【总结】
属性以灵活的方式实现了对私有字段的访问,是字段的自然扩展,一个属性总是与某个字段相关联,字段能干的,属性一定能干,属性能干的,字段不一定干的了;为了实现对字段的封装,保证字段的安全性,产生了属性,其本质是方法,暴露在外,可以对私有字段进行读写,以此提供对类中字段的保护,字段中存储数据更安全。
C# 索引器(Indexer)
索引器(Indexer) 允许一个对象可以像数组一样使用下标的方式来访问。
当您为类定义一个索引器时,该类的行为就会像一个 虚拟数组(virtual array) 一样。您可以使用数组访问运算符 [ ] 来访问该类的的成员。
语法:
c#
element-type this[int index]
{
// get 访问器
get
{
// 返回 index 指定的值
}
// set 访问器
set
{
// 设置 index 指定的值
}
}
- 所有索引器都使用this关键词来取代方法名。Class或Struct只允许定义一个索引器,而且总是命名为this。
- 索引器允许类或结构的实例按照与数组相同的方式进行索引。索引器类似于属性,不同之处在于它们的访问器采用参数。
- get 访问器返回值。set 访问器分配值。
- value 关键字用于定义由 set 索引器分配的值。
- 索引器不必根据整数值进行索引,由您决定如何定义特定的查找机制
索引器Vs数组
索引器的索引值不受类型限制。用来访问数组的索引值一定是整数,而索引器可以是其他类型的索引值。
索引器允许重载,一个类可以有多个索引器。
索引器不是一个变量没有直接对应的数据存储地方。索引器有get和set访问器。
索引器Vs属性
标示方式:属性以名称来标识,索引器以函数签名来标识。
索引器可以被重载。属性则不可以被重载。
属性可以为静态的,索引器属于实例成员,不能被声明为static
实例:
c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IndexTest
{
public class Person
{
// 定义两个字段信息
private string name;
private string password;
// 定义一个Name属性来操作name字段
public string Name
{
set { name = value; }
get { return name; }
}
// 定义一个Password属性来操作password字段
public string Password
{
set { password = value; }
get { return password; }
}
// 定义索引器,name字段的索引值为0,password字段的索引值为1
public string this[int index]
{
set
{
if (index == 0) name = value;
else if (index == 1) password = value;
}
get
{
if (index == 0) return name;
else if (index == 1) return password;
return null;
}
}
}
class IndexTest
{
static void Main(string[] args)
{
// 声明并且实例化类
Person p = new Person();
// 使用索引器的方式来给类的两个字段赋值
p[0] = "majin";
p[1] = "1234";
Console.WriteLine(p.Name);
Console.WriteLine(p.Password);
Console.ReadKey();
}
}
}
C# 委托(Delegate)
委托是什么?
委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。
🔸 从数据结构来讲,委托是和类一样是一种用户自定义类型。
🔸 从设计模式来讲,委托(类)提供了方法(对象)的抽象。
既然委托是一种类型,那么它存储的是什么数据?
我们知道,委托是方法的抽象,它存储的就是一系列具有相同签名和返回回类型的方法的地址。调用委托的时候,委托包含的所有方法将被执行。
C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。
所有的委托(Delegate)都派生自 System.Delegate 类。
为什么不用指针而用委托呢?
🔸 指针不安全:
- 前未初始化.用后未释放,造成内存泄漏
- 调用了一个已经释放不存在的指针
- 越界的问题
🔸 委托更加安全
委托有什么用处
在c#中,把一个函数传递到另外一个函数里执行,不能直接把方法名传递进去。这时候就需要用到了委托
主要是为了能传递函数,如回调!!!
1、委托有很好的封装性
2、委托的实例化与它的执行是在不同的对象中完成的
委托的声明和定义
实际上,委托在编译的时候确实会编译成类。因为Delegate是一个类,所以在任何可以声明类的地方都可以声明委托。
c#
//语法
delegate <return type> <delegate-name> <parameter list>
//实例
public delegate int MyDelegate (string s);
实例化委托
🔸 使用new运算符,不能为空
c#
public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
实例1:
c#
using System;
delegate int NumberChanger(int n);
namespace ConsoleApplication
{
class DelegateApp
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main()
{
// 创建委托实例
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
// 使用委托对象创建调用方法
nc1(25);
Console.WriteLine("num:{0}", getNum());
nc2(5);
Console.WriteLine("num:{0}", getNum());
Console.ReadKey();
}
}
}
委托的多播
🔸使用委托可以将多个方法绑定到同一个委托变量,当调用此变量时(这里用"调用"这个词,是因为此变量代表一个方法),可以依次调用所有绑定的方法。
🔸 委托对象可使用 "+" 运算符进行合并。
🔸 一个合并委托调用它所合并的两个委托。
🔸 只有相同类型的委托可被合并。
🔸 "-" 运算符可用于从合并的委托中移除组件委托。
c#
static void Main()
{
// 创建委托实例
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5); // (10+5) *5
Console.WriteLine("num:{0}", getNum());
Console.ReadKey();
}
实例2:
c#
using System;
namespace delegateAndevent2
{
//定义委托,他定义了可以代表的方法类型和参数列表
public delegate void GreetingDelegate(string name);
class delegateAndevent2
{
public static void EnglishGreeting(string name)
{
Console.WriteLine("Hello,{0}", name);
}
public static void ChineseGreeting(string name)
{
Console.WriteLine("早上好,{0}", name);
}
public static void GreetPeople(string name,GreetingDelegate MakeGreeting)
{
MakeGreeting(name);
}
/*
// 委托直接作为参数传入
static void Main()
{
GreetPeople("MaJin", EnglishGreeting);
GreetPeople("马锦", ChineseGreeting);
Console.ReadKey();
}
*/
/* // 将方法绑定到委托
static void Main(string[] args)
{
GreetingDelegate delegate1, delegate2;
delegate1 = EnglishGreeting;
delegate2 = ChineseGreeting;
GreetPeople("MaJin", delegate1);
GreetPeople("马锦", delegate2);
Console.ReadKey();
}*/
/* // 委托的多播
// 可以将多个方法赋给同一个委托,或者叫将多个方法绑定到同一个委托,当调用这个委托的时候,将依次调用其所绑定的方法。
static void Main(string[] args)
{
GreetingDelegate delegate1;
delegate1 = EnglishGreeting;
delegate1 += ChineseGreeting;
GreetPeople("MaJin", delegate1);
Console.ReadKey();
}*/
/*
// 也可以绕过GreetPeople方法,通过委托来直接调用EnglishGreeting和ChineseGreeting:
static void Main(string[] args)
{
GreetingDelegate delegate1;
delegate1 = EnglishGreeting;
delegate1 += ChineseGreeting;
delegate1("MaJin");
Console.ReadKey();
}*/
/*
// 委托的减播&&new
static void Main(string[] args)
{
GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting); // 必须带有参数,否则编译错误
delegate1 += ChineseGreeting;
delegate1 -= EnglishGreeting;
delegate1("MaJin");
Console.ReadKey();
}*/
}
}
C# 事件(Event)
事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。
C# 中使用事件机制实现线程间的通信。
声明事件(Event)
在类的内部声明事件,首先必须声明该事件的委托类型。例如:
public delegate void BoilerLogHandler(string status);
然后,声明事件本身,使用 event 关键字:
c#
// 基于上面的委托定义事件
public event BoilerLogHandler BoilerEventLog;
🔸类中的事件一定是私有的,不允许在类的外面以赋值的方式访问
事件的运用总结
事件的整个过程是:订阅 -> 发布 -> 执行。
- 订阅 : 假设事件 A 的执行方法是 F_A,事件 B 的执行方法是 F_B,将这些事件与它们的委托人 进行绑的行为就是订阅,这个委托人就是发布器的一个成员。订阅器另一个行为就是在订阅之后(必学先订阅)通知发布器的相关成员。
- 发布 : 首先要明确事件发布的类型(由事件的执行方法参数列表决定)和要发布事件的变量(这个变量即委托人);其次整理发布所需的材料、判断条件是否合适等;最后让内部的委托人向执行函数传递最终信息。
- 执行: 是整个事件最后完成的步骤,就是普通的函数。
事件vs委托
在.NET Framework中,委托(Delegate)和事件(Event)虽然紧密相关,但它们在概念和使用上有一些关键的区别:
- 定义 :
- 委托:是一种类型,可以持有对具有特定签名的方法的引用。委托可以实例化,并且可以像方法一样被调用。
- 事件:是基于委托的一种特殊成员,用于对象之间的松耦合通信。事件允许对象(发布者)通知多个对象(订阅者)某些事情的发生。
- 使用目的 :
- 委托:用于将方法作为参数传递给其他方法,或者将方法赋值给变量,以便稍后调用。
- 事件:用于实现发布-订阅模式,其中发布者在特定事件发生时通知所有订阅者。
- 访问级别 :
- 委托:可以被任何代码访问和使用,只要它具有相应的权限。
- 事件:通常作为类的公共或受保护成员公开,但只能在类的内部被触发(通过调用事件处理程序)。
- 触发方式 :
- 委托:可以直接调用或通过多播委托进行调用。
- 事件 :只能通过类的内部代码触发,通常是通过调用事件的
Invoke
方法或使用?.
语法进行条件调用。
- 订阅和取消订阅 :
- 委托:没有内置的订阅和取消订阅机制。
- 事件 :提供了
+=
和-=
运算符来订阅和取消订阅事件。
- 安全性 :
- 委托:在多线程环境中,委托的使用需要特别注意线程安全问题。
- 事件 :.NET 事件模型提供了一定程度的线程安全性,特别是在使用
lock
语句时。
- 自定义数据 :
- 委托:委托本身不包含任何关于调用上下文的信息。
- 事件 :事件可以通过自定义的
EventArgs
类传递额外的数据给订阅者。
- 封装性 :
- 委托:委托本身是一个简单的类型,不包含任何封装逻辑。
- 事件:事件可以通过访问修饰符和封装逻辑(如虚方法)提供封装性。
- 多播行为 :
- 委托:本身就是多播的,可以同时引用多个方法。
- 事件:虽然本质上也是多播的,但提供了额外的机制来控制事件的触发和订阅者的行为。
- 设计模式 :
- 委托:更多地与函数式编程范式相关。
- 事件:与观察者设计模式相关,用于实现对象之间的松耦合通信。
总结来说,委托是一种类型,用于引用方法,而事件是基于委托的机制,用于实现对象之间的通信。事件提供了一种安全、封装的方式来触发和订阅方法调用,而委托提供了一种灵活的方式来传递和调用方法。
事件有什么用处?
事件其实没什么不好理解的,声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。
委托的实例2中三个方法都定义在Programe类中,这样做是为了理解的方便,实际应用中,通常都是 GreetPeople 在一个类中,ChineseGreeting和 EnglishGreeting 在另外的类中。现在你已经对委托有了初步了解,是时候对上面的例子做个改进了。假设我们将GreetingPeople()放在一个叫GreetingManager的类中,那么新程序应该是这个样子的:
c#
using System;
using delegateAndevent3;
namespace delegateAndevent3
{
public delegate void GreetingDelegate(string name);
public class GreetingManager
{
public void GreetPeople(string name,GreetingDelegate MarkeGreeting)
{
MarkeGreeting(name);
}
}
class delegateAndevent3
{
public static void EnglishGreeting(string name)
{
Console.WriteLine("Hello,{0}", name);
}
public static void ChineseGreeting(string name)
{
Console.WriteLine("早上好,{0}", name);
}
static void Main()
{
GreetingManager gm = new GreetingManager();
gm.GreetPeople("MaJin", EnglishGreeting);
gm.GreetPeople("马锦", ChineseGreeting);
Console.ReadKey();
}
}
}
到了这里,我们不禁想到:面向对象设计,讲究的是对象的封装,既然可以声明委托类型的变量(在上例中是delegate1),我们何不将这个变量封装到 GreetManager类中?在这个类的客户端中使用不是更方便么?于是,我们改写GreetManager类,像这样:
c#
using System;
using delegateAndevent3;
namespace delegateAndevent3
{
public delegate void GreetingDelegate(string name);
public class GreetingManager
{
//在GreetingManager类的内部声明delegate1变量
public GreetingDelegate delegate1;
public void GreetPeople(string name,GreetingDelegate MarkeGreeting)
{
MarkeGreeting(name);
}
}
class delegateAndevent3
{
public static void EnglishGreeting(string name)
{
Console.WriteLine("Hello,{0}", name);
}
public static void ChineseGreeting(string name)
{
Console.WriteLine("早上好,{0}", name);
}
static void Main()
{
GreetingManager gm = new GreetingManager();
gm.delegate1 = EnglishGreeting;
gm.delegate1 += ChineseGreeting;
gm.GreetPeople("MaJin", gm.delegate1);
Console.ReadKey();
}
}
}
尽管这样做没有任何问题,但我们发现这条语句很奇怪。在调用gm.GreetPeople方法的时候,再次传递了gm的delegate1字段:
c#
public class GreetingManager{
//在GreetingManager类的内部声明delegate1变量
public GreetingDelegate delegate1;
public void GreetPeople(string name) {
if(delegate1!=null){ //如果有方法注册委托变量
delegate1(name); //通过委托调用方法
}
}
}
// 在客户端,调用看上去更简洁一些:
static void Main(string[] args) {
GreetingManager gm = new GreetingManager();
gm.delegate1 = EnglishGreeting;
gm.delegate1 += ChineseGreeting;
gm.GreetPeople("MaJin"); //注意,这次不需要再传递 delegate1变量
}
我们知道,并不是所有的字段都应该声明成public,合适的做法是应该public的时候public,应该private的时候private。
我们先看看如果把 delegate1 声明为 private会怎样?结果就是:这简直就是在搞笑。因为声明委托的目的就是为了把它暴露在类的客户端进行方法的注册,你把它声明为private了,客户端对它根本就不可见,那它还有什么用?
再看看把delegate1 声明为 public 会怎样?结果就是:在客户端可以对它进行随意的赋值等操作,严重破坏对象的封装性。
所以需要对delegate1进行封装 ,于是,Event出场了,它封装了委托类型的变量,使得:在类的内部,不管你声明它是public还是protected,它总是private的。在类的外部,注册"+="和注销"-="的访问限定符与你在声明事件时使用的访问符相同。
c#
using System;
using delegateAndevent3;
namespace delegateAndevent3
{
public delegate void GreetingDelegate(string name);
public class GreetingManager
{
//在GreetingManager类的内部声明一个事件,是私有的,无论声明的范围是什么
public event GreetingDelegate MakeGreet;
public void GreetPeople(string name)
{
MakeGreet(name);
}
}
class delegateAndevent3
{
public static void EnglishGreeting(string name)
{
Console.WriteLine("Hello,{0}", name);
}
public static void ChineseGreeting(string name)
{
Console.WriteLine("早上好,{0}", name);
}
static void Main()
{
GreetingManager gm = new GreetingManager();
// gm.MakeGreet = EnglishGreeting; // 编译错误1,事件是私有的,只能进行add和remove
gm.MakeGreet += ChineseGreeting;
gm.GreetPeople("MaJin");
Console.ReadKey();
}
}
}
委托、事件与Observer设计模式
假设我们有个高档的热水器,我们给它通上电,当水温超过95度的时候:
- 1、扬声器会开始发出语音,告诉你水的温度;
- 2、液晶屏也会改变水温的显示,来提示水已经快烧开了。
- 热水器类:Heater
- 字段:水温temperature
- 方法:加热BoilWater()、语音警报MakeAlert(),显示水温ShowMsg()。
c#
using System;
using delegateAndevent3;
using delegateAndevent4;
/*假设我们有个高档的热水器,我们给它通上电,当水温超过95度的时候:
* 1、扬声器会开始发出语音,告诉你水的温度;
* 2、液晶屏也会改变水温的显示,来提示水已经快烧开了。
* 使用文档:
* 热水器类:Heater
* 字段:水温temperature
* 方法:加热BoilWater()、语音警报MakeAlert(),显示水温ShowMsg()。
*/
namespace delegateAndevent4
{
class Heater
{
private int temperature;
public void BoilWater()
{
for (int i = 0; i < 100; i++)
{
temperature = i;
}
if (temperature > 95)
{
MakeAlert(temperature);
ShowMsg(temperature);
}
}
public void MakeAlert(int param)
{
Console.WriteLine("MakeAlert:嘀嘀嘀,水已经{0}度了", param);
}
public void ShowMsg(int param)
{
Console.WriteLine("ShowMsg:水快开了,当前温度{0}度。", param);
}
}
class delegateAndevent4
{
static void Main()
{
Heater ht = new Heater();
ht.BoilWater();
Console.ReadKey();
}
}
}
Observer设计模式简介和实例
上面的例子显然能完成我们之前描述的工作,但是却并不够好。现在假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么,应该是热水器 仅仅负责烧水,它不能发出警报也不能显示水温;在水烧开时由警报器 发出警报、显示器显示提示和水温。
c#
// 热水器
public class Heater {
private int temperature;
// 烧水
private void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
}
}
}
// 警报器
public class Alarm{
private void MakeAlert(int param) {
Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:" , param);
}
}
// 显示器
public class Display{
private void ShowMsg(int param) {
Console.WriteLine("Display:水已烧开,当前温度:{0}度。" , param);
}
}
这里就出现了一个问题:如何在水烧开的时候通知报警器和显示器?在继续进行之前,我们先了解一下Observer设计模式,Observer设计模式中主要包括如下两类对象:
- Subject:监视对象,它往往包含着其他对象所感兴趣的内容。在本范例中,热水器就是一个监视对象,它包含的其他对象所感兴趣的内容,就是temprature字段,当这个字段的值快到100时,会不断把数据发给监视它的对象。
- Observer:监视者,它监视Subject,当Subject中的某件事发生的时候,会告知Observer,而Observer则会采取相应的行动。在本范例中,Observer有警报器和显示器,它们采取的行动分别是发出警报和显示水温。
在本例中,事情发生的顺序应该是这样的:
- 警报器和显示器告诉热水器,它对它的温度比较感兴趣(注册)。
- 热水器知道后保留对警报器和显示器的引用。
- 热水器进行烧水这一动作,当水温超过95度时,通过对警报器和显示器的引用,自动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法。
类似这样的例子是很多的,GOF对它进行了抽象,称为Observer设计模式:Observer设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新。Observer模式是一种松耦合的设计模式。
c#
using System;
using delegateAndevent3;
using delegateAndevent5;
/*假设我们有个高档的热水器,我们给它通上电,当水温超过95度的时候:
* 1、扬声器会开始发出语音,告诉你水的温度;
* 2、液晶屏也会改变水温的显示,来提示水已经快烧开了。
* 使用文档:
* 热水器类:Heater
* 字段:水温temperature
* 方法:加热BoilWater()、语音警报MakeAlert(),显示水温ShowMsg()。
*/
namespace delegateAndevent5
{
// 热水器
public class Heater
{
private int temperature;
public delegate void BoilHandler(int param); //声明委托
public event BoilHandler BoilEvent; //声明事件
// 烧水
public void BoilWater()
{
for (int i = 0; i <= 100; i++)
{
temperature = i;
if (temperature > 95)
{
if (BoilEvent != null)
{ //如果有对象注册
BoilEvent(temperature); //调用所有注册对象的方法
}
}
}
}
}
// 警报器
public class Alarm
{
public void MakeAlert(int param)
{
Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);
}
}
// 显示器
public class Display
{
public static void ShowMsg(int param)
{ //静态方法
Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", param);
}
}
class delegateAndevent5
{
static void Main()
{
Heater heater = new Heater();
Alarm alarm = new Alarm();
heater.BoilEvent += alarm.MakeAlert; //注册方法
// heater.BoilEvent += (new Alarm()).MakeAlert; //给匿名对象注册方法
heater.BoilEvent += Display.ShowMsg; //注册静态方法
heater.BoilWater(); //烧水,会自动调用注册过对象的方法
Console.ReadKey();
}
}
}
符合.Net Framework规范的委托和事件
在C#中,事件处理程序的参数通常包括两个参数:object sender
和 EventArgs e
。其中,sender
参数代表触发事件的对象,而 e
是一个包含事件数据的参数。
c#
//.Net Framework 声明委托、声明事件
public delegate void BoiledEventHandler(object sender, EventArgs e);
public event BoiledEventHandler Boiled;
.Net Framework的编码规范:
- 委托的定义 :
- 委托是一种特殊的类型,它可以用来引用方法。它允许你将方法作为参数传递给其他方法,或者将方法赋值给变量。
- 委托的声明 :
- 委托通常与事件相关联,并且定义了事件处理程序的签名。
- 事件的声明 :
- 事件是某个类可以触发的信号,其他对象可以订阅这些信号。
- 事件的实现 :
- 事件通常通过定义一个私有的委托类型的字段来实现,然后提供添加(Subscribe)和移除(Unsubscribe)事件的方法。
- 事件的触发 :
- 当事件发生时,使用委托字段调用所有订阅者的方法。
- 事件的订阅和取消订阅 :
- 客户端代码可以使用
+=
运算符订阅事件,使用-=
运算符取消订阅。
- 客户端代码可以使用
- 事件的封装 :
- 事件应该封装在类中,并且通常作为公共属性公开,允许外部代码订阅和取消订阅。
- 使用自定义的EventArgs :
- 当需要传递事件数据时,可以创建从
EventArgs
继承的自定义类。
- 当需要传递事件数据时,可以创建从
- 线程安全 :
- 在多线程环境中,事件的添加和移除应该是线程安全的。
- 避免内存泄漏 :
- 确保在不再需要事件订阅时取消订阅,以避免内存泄漏。
- 避免直接调用事件处理程序 :
- 直接调用事件处理程序可能会导致订阅者之间的意外交互,应该通过事件委托来触发事件。
- 使用虚拟方法进行事件触发 :
- 在基类中使用
protected virtual
方法来触发事件,允许派生类重写此方法以自定义行为或取消事件触发。
- 在基类中使用
- 事件命名 :
- 事件名称通常使用名词或名词短语,以
Event
结尾,例如DataAvailableEvent
。
- 事件名称通常使用名词或名词短语,以
- 委托和事件的匹配 :
- 委托的签名必须与事件处理程序的签名完全匹配。
- 避免过度使用事件 :
- 事件应该用于松耦合的组件之间的通信,而不是用于流程控制或简单的方法调用。
🔸委托声明原型中的Object类型的参数代表了Subject,也就是监视对象,在本例中是 Heater(热水器)。回调函数(比如Alarm的MakeAlert)可以通过它访问触发事件的对象(Heater)。
🔸EventArgs 对象包含了Observer所感兴趣的数据,在本例中是temperature。
**上面这些其实不仅仅是为了编码规范而已,这样也使得程序有更大的灵活性。**比如说,如果我们不光想获得热水器的温度,还想在Observer端(警报器或者显示器)方法中获得它的生产日期、型号、价格,那么委托和方法的声明都会变得很麻烦,而如果我们将热水器的引用传给警报器的方法,就可以在方法中直接访问热水器了。
c#
using System;
using delegateAndevent3;
using delegateAndevent6;
/*假设我们有个高档的热水器,我们给它通上电,当水温超过95度的时候:
* 1、扬声器会开始发出语音,告诉你水的温度;
* 2、液晶屏也会改变水温的显示,来提示水已经快烧开了。
* 3、需要显示热水器的生产日期、型号、价格
* 使用文档:
* 热水器类:Heater
* 字段:水温temperature
* 方法:加热BoilWater()、语音警报MakeAlert(),显示水温ShowMsg()。
*/
namespace delegateAndevent6
{
// 热水器
public class Heater
{
private int temperature;
public string type = "No 001";
public string area = "China Xian";
//.Net Framework 声明委托、声明事件
public delegate void BoiledEventHandler(object sender, BoiledEventArgs e);
public event BoiledEventHandler Boiled;
// 定义BoiledEventArgs类,传递给Observer所感兴趣的信息
public class BoiledEventArgs : EventArgs // 继承EventArgs类
{
public readonly int temperature;
public BoiledEventArgs(int temperature)
{
this.temperature = temperature;
}
}
// 可以供继承自Heater的类重写,以便继承类拒绝其他对象对它的监视
protected virtual void OnBoiled(BoiledEventArgs e)
{
if (Boiled != null)
{
Boiled(this, e); // 调用所有注册对象的方法
}
}
// 烧水。
public void BoilWater()
{
for (int i = 0; i <= 100; i++)
{
temperature = i;
if (temperature > 95)
{
//建立BoiledEventArgs 对象。
BoiledEventArgs e = new BoiledEventArgs(temperature);
OnBoiled(e); // 调用 OnBolied方法
}
}
}
}
// 警报器
public class Alarm
{
// sender 参数代表触发事件的对象
public void MakeAlert(Object sender, Heater.BoiledEventArgs e)
{
Heater heater = (Heater)sender; // 为了在事件处理程序中访问 Heater 类特有的成员,需要将 sender 转换为 Heater 类型
Console.WriteLine("Alarm:{0} - {1} ", heater.area, heater.type);
Console.WriteLine("Alarm: 嘀嘀嘀,水已经{0}度了", e.temperature);
Console.WriteLine();
}
}
// 显示器
public class Display
{
public static void ShowMsg(Object sender, Heater.BoiledEventArgs e)
{
Heater heater = (Heater)sender;
Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);
Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", e.temperature);
Console.WriteLine();
}
}
class delegateAndevent6
{
static void Main()
{
Heater heater = new Heater();
Alarm alarm = new Alarm();
heater.Boiled += alarm.MakeAlert; // 注册方法
//heater.Boiled += (new Alarm()).MakeAlert; //给匿名对象注册方法
//heater.Boiled += new Heater.BoiledEventHandler(alarm.MakeAlert); //也可以这么注册
heater.Boiled += Display.ShowMsg; //注册静态方法
heater.BoilWater(); //烧水,会自动调用注册过对象的方法
Console.ReadKey();
}
}
}
C# 集合
C# 中的集合是存储数据的一种方式,它们可以包含一个或多个元素。C# 提供了多种集合类型,每种类型都有其特定的用途和特性。
常用的 C# 集合类型
-
数组(Array):
c#// 数组 // 1.具有固定大小的元素集合,声明时必须指定大小和类型。 // 2.访问元素时使用索引。 int[] numbers = new int[5] { 1, 2, 3, 4, 5 }; int number = numbers[0]; // 访问第一个元素
-
列表(List):
c#// 列表 // 1.List<T> 是一个动态数组,可以根据需要自动调整大小。 // 2.可以包含重复的元素,支持索引访问。 List<int> list = new List<int> { 1, 2, 3 }; list.Add(4); // 添加元素 int firstItem = list[0]; // 访问第一个元素
-
字典(Dictionary):
c#// 字典 // 1.Dictionary<TKey, TValue> 是一个键值对集合,每个元素包含一个键和一个值。 // 2. 通过键来访问元素,键必须是唯一的。 Dictionary<string, int> scores = new Dictionary<string, int> { { "Alice", 90 }, { "Bob", 85 } }; int aliceScore = scores["Alice"]; // 通过键访问元素
-
集合(HashSet):
c#// 集合 // 1.HashSet<T> 是一个不允许重复元素的集合。 // 2. 提供了添加、移除和查找元素的方法。 HashSet<int> uniqueNumbers = new HashSet<int> { 1, 2, 3, 4 }; uniqueNumbers.Add(5); // 添加元素
-
栈(Stack):
c#// 栈 // 1.Stack<T>是一个后进先出(LIFO)的集合。 // 2. 只能在栈顶添加或移除元素。 Stack<int> stack = new Stack<int>(); stack.Push(1); // 添加元素到栈顶 int topItem = stack.Pop(); // 移除并返回栈顶元素
-
队列(Queue):
c#// 队列 // 1.Queue<T>是一个先进先出(FIFO)的集合。 // 2. 在队列的末尾添加元素,在前端移除元素。 Queue<int> queue = new Queue<int>(); queue.Enqueue(1); // 在队列末尾添加元素 int firstInLine = queue.Dequeue(); // 移除并返回队列前端元素
-
链表(LinkedList):
c#// 链表 : 允许快速插入和删除元素,但访问特定元素可能较慢。 LinkedList<int> linkedList = new LinkedList<int>(); linkedList.AddLast(1); // 在链表末尾添加元素 int firstNodeValue = linkedList.First.Value; // 访问第一个节点的值
-
排序列表(SortedList):
c#// 排序列表 // 1.SortedList<TKey, TValue> 是一个有序的键值对集合。 // 2. 元素按照键排序存储。 SortedList<string, int> sortedScores = new SortedList<string, int> { { "Alice", 90 }, { "Bob", 85 } }; int score = sortedScores["Alice"]; // 访问元素,列表保持排序
-
位数组(BitArray):
c#// 位数组 : `BitArray` 是一个位的集合,可以设置和检查单个位的状态。 BitArray bitArray = new BitArray(8); bitArray[0] = true; // 设置第一个位 bool isSet = bitArray[0]; // 检查第一个位的状态
使用场景:
- 集合的大小是否固定。
- 是否需要快速访问元素。
- 是否需要避免重复元素。
- 元素的添加和删除操作的频率。
C# 泛型(Generic)
什么是泛型?
字面意思像 "泛滥的类型" 的意思
泛型(Generic),是将不确定的类型预先定义下来的一种C#高级语法,我们在使用一个类,接口或者方法前,不知道用户将来传什么类型,或者我们写的类,接口或方法相同的代码可以服务不同的类型,就可以定义为泛型。这会大大简化我们的代码结构,同时让后期维护变得容易。
泛型很适用于集合,我们常见的泛型集合有:List<T>,Dictionary<K,V>
等等(T,K,V就代表不确定的类型,它是一种类型占位符),无一不是利用的泛型这一特性,若没有泛型,我们会多出很多重载方法,以解决类型不同,但是执行逻辑相同的情况。
定义泛型:
-
类型参数 : 使用单个字母(通常是
T
)作为类型参数来定义泛型。c#public class MyClass<T> { }
-
泛型类: 定义一个泛型类,可以在类名后指定类型参数。
c#public class Stack<T> { private T[] items = new T[10]; // 类的实现... }
-
泛型接口: 类似于泛型类,泛型接口使用类型参数。
c#public interface IComparer<T> { int Compare(T x, T y); }
-
泛型方法: 你可以在方法中定义泛型,允许方法使用任何类型的参数。
c#public class Utils { public static T FindMax<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) > 0 ? a : b; } }
-
泛型委托:泛型委托和普通委托差别不大,只是参数由具体类型变成了 "T"
c#using System; namespace genDelegate { class genDelegate { delegate void SayHelloDelegate<T>(T args); static void Say(string msg) { Console.WriteLine(msg); } static void Main() { SayHelloDelegate<string> myDelegate = Say; myDelegate("hello"); Console.ReadKey(); } } }
泛型实例:
我们需要封装一个加法的算法,但是传入的类型,可能有 int 类型,也可能有 string 类型,还可能有其他的类型
c#
using System;
namespace generic
{
public class Test
{
public static int Add(int a, int b)
{
return a + b;
}
public static int Add(string a, string b) {
return int.Parse(a) + int.Parse(b);
}
public static int AddByGen<T>(T a,T b)
{
try
{
return int.Parse(a.ToString()) + int.Parse(b.ToString());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return -1;
}
}
class generic
{
static void Main()
{
int result1 = Test.AddByGen(1, 2);
int result2 = Test.AddByGen("3", "4");
Console.WriteLine("res1={0},res2={1}",result1,result2);
Console.ReadKey(true);
}
}
}
为什么要用泛型?
- 类型安全:泛型允许你定义操作特定数据类型的类和方法,而无需在代码中进行类型转换。这减少了类型错误的可能性,并提高了代码的可读性。
- 代码重用:泛型使得你可以编写出可以用于多种数据类型的通用算法和数据结构,而无需为每种数据类型编写特定的代码。
- 性能:泛型避免了装箱和拆箱操作,因为它们直接操作指定的数据类型,这可以提高性能,尤其是在处理值类型(如整数、浮点数等)时。
- 编译时类型检查:泛型在编译时执行类型检查,这意味着与类型相关的错误可以在编译时被捕获,而不是在运行时。
- 简化API设计:泛型使得API设计更加简洁和灵活,因为它们可以接受任何类型的参数,返回任何类型的值。
- 协变和逆变:泛型接口和委托支持协变和逆变,这允许你使用更具体的类型替换泛型类型参数,从而提供更大的灵活性。
- 集合类 :泛型使得可以创建类型安全的集合类,如
List<T>
、Dictionary<TKey, TValue>
等,这些集合类在.NET Framework中广泛使用。 - 限制和约束:泛型可以使用类型约束来限制泛型参数可以是哪些类型的实例,例如类类型、接口实现,甚至是无界类型参数。
- 减少运行时类型识别 :泛型减少了需要使用
is
和as
运算符进行运行时类型识别的需求。 - 支持LINQ:泛型与LINQ(语言集成查询)紧密集成,使得可以编写出强大且类型安全的查询。
- 设计模式:泛型使得实现某些设计模式(如工厂模式、策略模式等)更加灵活和安全。
泛型和Object类型的区别
C# 中 Object 是一切类型的基类,可以用来表示所有类型,而泛型是指将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。
🔸 作用时机
- 泛型:以将泛型理解成替换,在使用的时候将泛型参数替换成具体的类型,这个过程是在编译的时候进行的,使用泛型编译器依然能够检测出类型错误。
object
类型:Object 表示其他类型是通过类型转换来完成的,而所有类型转化为 Object 类型都是合法的,所以即使你先将 Object 对象赋值为一个整数再赋值为一个字符串,编译器都认为是合法的。
🔸 性能:
- 泛型 :由于泛型是类型安全的,它们避免了装箱和拆箱操作,这可以提高性能。特别是对于值类型(如
int
、double
等),使用泛型可以避免不必要的内存分配。 object
类型 :当存储值类型时,可能会导致装箱和拆箱,这会引入额外的性能开销。装箱是将值类型转换为object
类型或接口类型的过程,拆箱是相反的过程。
🔸 代码重用:
- 泛型:允许你编写可重用的代码,这些代码可以用于多种数据类型,而不需要为每种类型编写特定的实现。
object
类型:虽然也可以用于编写通用的代码,但由于缺乏类型安全,可能需要在使用时进行类型转换,这降低了代码的重用性
🔸 可读性和维护性:
- 泛型:提供了更好的可读性和维护性,因为它们清楚地表明了期望的数据类型。
object
类型:由于使用了基类引用,可能使代码难以阅读和维护,尤其是在复杂的系统中。
🔸协变和逆变:
- 泛型:泛型接口和委托支持协变和逆变,这允许你使用更具体的类型替换泛型类型参数。
object
类型:不支持协变和逆变,类型关系更加固定。
🔸使用场景:
- 泛型:适用于需要类型安全和性能的场景,如集合类、算法实现等。
object
类型:适用于需要处理未知类型或在不知道具体类型的情况下编写通用代码的场景。
泛型约束:
在C#中,泛型约束(Generic Constraints)用于限制泛型参数的类型,确保它们满足特定的要求。这使得编译器能够强制执行泛型参数的一致性和兼容性,从而提高代码的类型安全性和灵活性。
-
类类型约束(Class Constraint): 限制泛型参数必须是引用类型。这允许你调用对象的成员,如方法和属性。
c#csharppublic class MyClass<T> where T : class { public void Method() { // T 必须是一个引用类型 } }
-
结构体约束(Struct Constraint): 限制泛型参数必须是值类型。
c#csharppublic struct MyStruct<T> where T : struct { // T 必须是一个值类型 }
-
新约束(New Constraint): 确保泛型参数具有无参数的公共构造函数,允许创建其实例。
c#csharppublic class MyClass<T> where T : new() { public T CreateInstance() => new T(); }
-
接口约束: 要求泛型参数实现特定的接口。
c#csharppublic interface IExample { void Method(); } public class MyClass<T> where T : IExample { public void ExecuteMethod(T item) { item.Method(); } }
-
多个约束: 可以对泛型参数应用多个约束。
c#csharppublic class MyClass<T> where T : class, IExample, new() { // T 必须是引用类型,实现 IExample 接口,并且具有无参数的构造函数 }
-
基类约束: 要求泛型参数继承自特定的基类。
c#csharppublic class BaseClass { public virtual void Method() { } } public class MyClass<T> where T : BaseClass { public void ExecuteMethod(T item) { item.Method(); } }
泛型配合反射:
现在有一个需求,用户修改个人资料,在提交这里必须要判断是否有修改内容,如果没有修改,点击提交按钮则不提交,那要怎么判断呢?
有人可能会说,用 if...else 去判断就好了,可以这么写没错,但资料如果有几百个地方改了,不会还用 if...else 吧,那整篇代码全是 if...else 了,就像我同事开玩笑一样:"我不会高数,我只会写 if...else..."
下面这个案例就教你如何来解决这个问题。
c#
using System;
using System.Reflection;
namespace genReflection
{
// 用一个类来进行比较
public class Test
{
public string Names { get; set; }
public int Age{ get; set; }
public float Height{ get; set; }
}
class genReflection
{
public static bool CompareType<T>(T oneT, T twoT)
{
bool result = true;//两个类型作比较时使用,如果有不一样的就false
Type typeOne = oneT.GetType();
Type typeTwo = twoT.GetType();
//如果两个T类型不一样 就不作比较
if (!typeOne.Equals(typeTwo)) { return false; }
PropertyInfo[] pisOne = typeOne.GetProperties(); //获取所有公共属性(Public)
PropertyInfo[] pisTwo = typeTwo.GetProperties();
//如果长度为0返回false
if (pisOne.Length <= 0 || pisTwo.Length <= 0)
{
return false;
}
//如果长度不一样,返回false
if (!(pisOne.Length.Equals(pisTwo.Length))) { return false; }
//遍历两个T类型,遍历属性,并作比较
for (int i = 0; i < pisOne.Length; i++)
{
//获取属性名
string oneName = pisOne[i].Name;
string twoName = pisTwo[i].Name;
//获取属性的值
object oneValue = pisOne[i].GetValue(oneT, null);
object twoValue = pisTwo[i].GetValue(twoT, null);
//比较,只比较值类型
if ((pisOne[i].PropertyType.IsValueType || pisOne[i].PropertyType.Name.StartsWith("String")) && (pisTwo[i].PropertyType.IsValueType || pisTwo[i].PropertyType.Name.StartsWith("String")))
{
if (oneName.Equals(twoName))
{
if (oneValue == null)
{
if (twoValue != null)
{
result = false;
break; //如果有不一样的就退出循环
}
}
else if (oneValue != null)
{
if (twoValue != null)
{
if (!oneValue.Equals(twoValue))
{
result = false;
break; //如果有不一样的就退出循环
}
}
else if (twoValue == null)
{
result = false;
break; //如果有不一样的就退出循环
}
}
}
else
{
result = false;
break;
}
}
else
{
//如果对象中的属性是实体类对象,递归遍历比较
bool b = CompareType(oneValue, twoValue);
if (!b) { result = b; break; }
}
}
return result;
}
/* static void Main()
{
Test test1 = new Test();
test1.Names = "张三";
test1.Age = 24;
test1.Height = 179.9f;
Test test2 = new Test();
test2.Names = "李四";
test2.Age = 31;
test2.Height = 163.4f;
bool result = CompareType(test1, test2);
Console.WriteLine("是否相同:"+result);
Console.ReadKey();
}*/
}
}
C# 匿名方法
什么是匿名方法?
匿名方法顾名思义就是这类方法的特点是不需要特别去定义函数的名字的。一般我们需要一个函数,但又不想花时间去命名它的时候,就可以使用匿名方法。在 C# 中, 匿名方法通常表现为使用 delegate运算符和 Lambda 表达式。
使用场景
匿名方法常用于事件处理、定时器回调、以及任何需要委托实例但不需要单独定义方法的情况。
delegate 运算符(不常用)
delegate 运算符 创建一个可以转换为委托类型的匿名方法。 匿名方法可以转换为System.Action 和 System.Func 等类型,同时指定自定义的参数列表。
c#
using System;
namespace AATemp
{
class anonymousDeleagte
{
static void Main()
{
// Func委托:最多可以指定16个类型参数,其中最后一个参数指定方法的返回类型,其余的参数指定方法的输入参数类型。
Func<string,string,string> hello = delegate(string s1,string s2) { return s1 + s2; };
Console.WriteLine(hello("hello", "majin"));
// Action委托:专门用于不需要返回结果的方法
Action greet = delegate { Console.WriteLine("你好!"); };
greet();
Console.ReadKey();
}
}
}
Lambda 表达式
Lambda 表达式是 C# 3.0 引入的一种简洁的匿名函数语法,允许你在需要时快速定义一个方法的行为,而无需遵循传统的方法定义。
Lambda 表达式提供了一种简洁和富有表现力的方式来创建匿名函数可以使用 => 运算符来构造 Lambda 表达式。
性能 : 与匿名方法delegate
相比,Lambda 表达式通常具有更好的性能,因为编译器可以优化它们的实现。
定义语法:
c#
(input-parameters) => { <sequence-of-statements> }
- input-parameters: 表示输入参数,也即参数列表。
- sequence-of-statements: 表示表达式语句序列部分。
示例:
c#
Action la1 = () => { Console.WriteLine("无参数了,无返回!"); };
la1();
Func<int,int> la2 = (x) => { return x * x; };
Console.WriteLine("有参数,有返回" + la2(5));
var judge = int (bool b) => b ? 1 : 2; // C#新特性:显示返回类型:必须返回int
省略模式
省略规则:
① 参数类型可以省略。但是有多个参数的情况下,不能只省略一个
② 如果参数有且仅有一个,那么小括号可以省略
③ 如果代码块的语句只有一条,可以省略大括号和分号,甚至是return
C# 多线程
什么是线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中。线程自身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。
使用线程节省了 CPU 周期的浪费,同时提高了应用程序的效率。
线程生命周期
程生命周期开始于 System.Threading.Thread 类的对象被创建时,结束于线程被终止或完成执行时。
下面列出了线程生命周期中的各种状态:
-
未启动状态:当线程实例被创建但 Start 方法未被调用时的状况。
-
就绪状态:当线程准备好运行并等待 CPU 周期时的状况。
-
不可运行状态
:下面的几种情况下线程是不可运行的:
- 已经调用 Sleep 方法
- 已经调用 Wait 方法
- 通过 I/O 操作阻塞
-
死亡状态:当线程已完成执行或已中止时的状况。
主线程
在 C# 中,System.Threading.Thread 类用于线程的工作。它允许创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为主线程。
当 C# 程序开始执行时,主线程自动创建。使用 Thread 类创建的线程被主线程的子线程调用。您可以使用 Thread 类的 CurrentThread 属性访问线程。
c#
using System;
using System.Threading;
namespace ThreadTest1
{
class ThreadTest1
{
static void Main()
{
Thread th = Thread.CurrentThread;
th.Name = "MainThread";
Console.WriteLine("ID:{0},{1}线程是否被激活?{2}",th.ManagedThreadId,th.Name,th.IsAlive);
Console.ReadKey();
}
}
}
线程的一些操作
-
创建线程:线程是通过扩展 Thread 类创建的。扩展的 Thread 类调用 Start() 方法来开始子线程的执行。
c#using System; using System.Threading; namespace ThreadTest2 { class ThreadTest2 { public static void CallToChildThread() { Console.WriteLine("Child thread starts"); } static void Main() { ThreadStart childref = new ThreadStart(CallToChildThread); Console.WriteLine("In Main: Creating the Child thread"); Thread childThread = new Thread(childref); childThread.Start(); Console.ReadKey(); } } }
-
管理线程
c#Thread.Sleep(5000); // 线程暂停 5000 毫秒
-
销毁线程:Abort()
c#using System; using System.Threading; namespace ThreadTest2 { class ThreadTest3 { public static void CallToChildThread() { try { Console.WriteLine("Child thread starts"); for (int i = 0; i <= 10; i++) { Thread.Sleep(500); Console.WriteLine("Child Thread Completed"); } } catch (ThreadAbortException e) { Console.WriteLine("Thread Abort Exception"); } finally { Console.WriteLine("Couldn't catch the Thread Exception"); } } static void Main() { ThreadStart childref = new ThreadStart(CallToChildThread); Console.WriteLine("In Main: Creating the Child thread"); Thread childThread = new Thread(childref); childThread.Start(); // 停止主线程一段时间 Thread.Sleep(2000); // 现在中止子线程 Console.WriteLine("In Main: Aborting the Child thread"); childThread.Abort(); Console.ReadKey(); } } }