1、委托简要复习
(0)委托是一种数据类型,像类一样的一种数据类型。一般都是在直接在命名空间定义。
(1)定义委托:需指明返回类型、委托名与参数列表。这样明确函数签名。
(2)使用委托:
a.声明委托变量
b.委托是一个引用类型,同类一样,若声明委托变量后不赋值则为null,所以在使
用委托变量前最好做非空检查!null
c.委托类型的变量只能赋值一个委托类型的对象。
MyDelegate md=new MyDelegate(M1);
//MyDelegate md=M1;//编译器编译时仍然是上面情况,这是简写
md.Invok();//md.Invok(参数)
md();//md(参数)
2、下列替换邮箱为什么出错:[email protected]>**@163.com或[email protected]>***@163.com
Regex r = new Regex(@"((\w-)+?)(@(\w-)+(\.(\w-)+)+)");
MatchCollection smail = r.Replace(input, delegate (Match m)
{
int n = m.Groups[1].Value.Length;
string s = new string('*', n);
return s + m.Groups[3].ToString();
});
原因,是replace返回的是一次性的string,而不是集合.
通过F12查看这个委托的原型是:
public delegate string MatchEvaluator(Match match);
固定输入参数是Match,返回参数是string,所以固定的委托类型是MatchEvaluator.
这个委托类型是系统定义好了的,所以直接使用。
问:上面delegate可以换成new delegate吗?
答:不行。
使用new delegate语法来创建委托实例已经过时,在较新的版本中已经不再支持。
正确的做法是使用更简洁的lambda表达式来创建MatchEvaluator委托实例。
string input = "[email protected]";
string rp = Regex.Replace(input, @"(\w+)(@[\w.]+)", (m) =>
{
int n = m.Groups[1].Value.Length;
string s = new string('*', n);
return string.Concat(s, m.Groups[2].Value);
});
也可以老实地用:
private static void Main(string[] args)
{
string input = "[email protected]";
string rp = Regex.Replace(input, @"(\w+)(@[\w.]+)", ReplaceMail);
Console.WriteLine(rp);
Console.ReadKey();
}
private static string ReplaceMail(Match m)
{
int n = m.Groups[1].Value.Length;
string s = new string('*', n);
return s + m.Groups[2].ToString();
}
问:字符串连接+与string.concat有什么区别?
答:一般情况,效果相同。但仅在大量字符连接有区别。
(1)运算符重载:
`+` 运算符在字符串连接时进行了运算符重载,它会调用 `string.Concat`
方法来实现字符串的连接。因此,实际上,使用 `+` 运算符进行字符串连接
与使用 `string.Concat` 方法是等效的。
(2)可变性:
`+` 运算符会创建一个新的字符串实例来容纳连接操作后的结果,每次连接都
会分配新的内存空间。这意味着,在多次连接字符串时,会导致多个中间字符
串的创建和内存分配。而 `string.Concat` 方法则会尽量减少不必要的中间
字符串的创建,它会在内部构建一个较大的字符串缓冲区,以便一次性连接所
有的字符串。
(3)可读性:
使用 `+` 运算符可以更直观地表示字符串的连接,代码可读性较好。而
`string.Concat` 方法在连接多个字符串时需要使用方法调用,可能会稍显
冗长。
总结:对于简单的字符串连接,直接使用 `+` 运算符是常见的做法,它简洁、易读。
而对于大量需要连接的字符串,使用 `string.Concat` 方法可以提供更好
的性能,因为它会进行更有效的内部处理以减少中间字符串的创建。
3、问:一般在什么地方定义委托类型?
答:此问不说作用范围,而着重说委托类型。
C#/.NET Framework提供了许多预定义的委托类型,用于处理各种常见的场景和功能。
这些预定义的委托类型包含在System命名空间中,并且可以直接使用,无需自己定义。
以下是一些常用的预定义委托类型:
Action: 用于表示没有返回值的方法,可以接受最多16个输入参数。
Func: 用于表示带有返回值的方法,可以接受最多16个输入参数。
Predicate: 用于表示返回布尔值的方法,通常用于筛选或判断条件。
EventHandler: 用于表示事件处理程序方法,常用于处理事件触发时的逻辑。
Comparison: 用于在进行排序或比较操作时定义比较逻辑。
这些预定义的委托类型非常方便,可以避免重复定义和冗余代码。它们提供了灵活
性和可重用性,使得编写代码更加简洁和可读。当然,如果预定义的委托类型无法满足
需求,如需要自定义委托类型的特定签名或功能,你仍然可以手动定义自己的委托类型。
4、问:委托变量在哪里定义?
答:哪儿用哪儿定义!
例如:前面窗体间传值。在Form1中使用,把委托变量传到Form2,调用时实则在Form1中。
委托变量的定义位置应该尽可能接近其使用位置,以便于理解和维护代码。你可以
在需要使用委托的地方进行定义,并将其传递给其他方法或类,在其他地方调用该委托
变量来执行相应的操作。
以下是几个常见的使用情况: (1)传递给方法:
如果你希望在一个方法中使用委托,可以在该方法内部定义委托变量,并将其作为
参数传递给其他方法。这样,在其他方法中调用委托变量时,会在定义它的方法中进行
使用。
public void Method1()
{
Action myDelegate = (x) => Console.WriteLine(x);//变量定义
Method2(myDelegate);
}
public void Method2(Action action)//变量使用
{
action?.Invoke(123);
}
Method1中定义了myDelegate委托变量,并将其作为参数传递给Method2方法。当在
Method2方法中调用委托变量时,会在Method1中的匿名方法中执行。
(2)传递给类的构造函数:
如果你需要在一个类的多个方法中使用同一个委托,可以将委托变量定义为类的
成员,并在类的构造函数中初始化。然后,可以在该类的其他方法中使用该委托变量。
public class MyClass
{
private Action myDelegate;//委托变量定义(成员)
public MyClass(Action action)
{
myDelegate = action;//使用
}
public void Method()
{
myDelegate?.Invoke(123);//使用
}
}
public static void Main(string[] args)
{
Action myDelegate = (x) => Console.WriteLine(x);
MyClass myClass = new MyClass(myDelegate);
myClass.Method();
}
通过在类的构造函数中传递委托变量,将委托与类进行关联。然后在类的其他方法中
使用委托变量来执行相应的操作。
总结:委托变量应该在离其使用位置最近的地方进行定义。这样可以提高代码的可读
性和可维护性,使得代码的逻辑更加清晰和易于理解。
1、使用Delegate的时候很多时候没必要使用一个普通的方法,
因为这个方法只有这个Delegate会用,并且只用一次,这时候使用匿名方法最合适。
匿名方法就是没有名字的方法。3就是没有名字的int对象。3+5就是两个匿名int对
象的相加,允许匿名对象,就允许匿名方法。
ProcessWordDelegate p = delegate(string s)
{
Console.WriteLine(s);
};
知道C#中有匿名方法,看到这种写法知道是匿名函数即可。
因为对于任意的匿名方法,都可以使用Lambda表达式来进行表示和替代。Lambda表
达式提供了一种更简洁、更易读的语法来定义匿名方法。
2、问:前面的delegate()是什么意思?
答:这是匿名委托,括号可跟参数。语法:delegate(参数)
匿名委托的语法与常规委托的定义类似,区别在于没有明确指定方法的名称,而是
直接在委托的定义中提供方法的实现。
但是,从C# 2.0开始,可以使用更简洁的Lambda表达式来替代匿名委托,如前面提
到的例子。Lambda表达式提供了一种更简洁、易读的方式来定义匿名方法。
问:delegate匿名函数的参数的数据类型可以省略吗?
答:匿名函数中的参数类型可以省略,前提是能够通过上下文推断出参数的类型;否则,
需要显式指定参数类型。
MyDelegate myDelegate = delegate(x)
{
Console.WriteLine(x);
};
上面可以推断出x为int,所以可以省略x的类型定义。lambda表达式中参数同理。
3、举例
有参但无返回值:
public delegate void MyDelegate1(string s);
internal class Program
{
private static void Main(string[] args)
{
MyDelegate1 md1 = delegate (string s)
{
Console.WriteLine($"早上好,{s},没有返回值!");
};
md1("kitty");
Console.ReadKey();
}
}
有参有返回值:
public delegate int MyDelegate2(int n1, int n2, int n3);//a
internal class Program
{
private static void Main(string[] args)
{
MyDelegate2 md2 = delegate (int n1, int n2, int n3)
{
return n1 + n2 + n3;
};
Console.WriteLine(md2(1, 2, 3));
Console.ReadKey();
}
}
问:上面a处为什么不用params int[]参数呢?
答:取决于你的需求。如果你只用一次委托,定义params的确没必要,若要使用多次,
且参数变化,建议还是用params为好。上面明显只用一次,故不用为好。
问:如果多次调用一个委托,是用匿名委托好,还是定义一个委托变量好?
答:在多次调用一个委托的情况下,使用匿名委托和定义一个委托变量都是可行的,取
决于你的需求和代码的可读性。
使用匿名委托时,你可以直接在调用点定义委托并传递给方法或事件。这样可以省
去定义另一个委托变量的步骤,减少了代码的复杂性。特别是当你只需要在特定的上下
文中使用这个委托时,使用匿名委托可能更为简洁。
但是,如果你在多个地方使用相同的委托,或者希望为委托提供更明确的名称和类
型定义,那么定义一个委托变量可能更合适。这样可以提高代码的可读性和可维护性,
通过名称来表达委托的意图。
总结:使用匿名委托和定义委托变量都是根据你的需求和偏好来选择的。匿名委托
更适合于临时使用和简单场景,而定义委托变量则更适合于在多个地方使用相同的委托
和提高代码可读性的情况。
1、Lambda表达式的语法
(input parameters) => expression or statement block
input parameters:是方法的输入参数列表。
=>:是Lambda操作符,表示“goes to”。
expression or statement block:是方法的表达式或语句块,
可以是单个表达式或多个语句。
Lambda表达式可以代替匿名方法中的具体实现,并可以根据需要进行简化。
例1:使用Lambda表达式替代简单的匿名方法
Action printMessage = (message) => Console.WriteLine(message);
printMessage("Hello, world!");
例2:Lambda表达式与LINQ结合使用
List numbers = new List { 1, 2, 3, 4, 5 };
List evenNumbers = numbers.Where(x => x % 2 == 0).ToList();
例3:Lambda表达式作为参数传递给方法
void PerformOperation(Action action)
{
action(123);
}
PerformOperation((x) => Console.WriteLine(x));
上面lambda表达式作为一个方法参数a,传入PerformOperation方法中,去执行这个方法a
Lambda表达式可以在各种场景中替代匿名方法,使代码更加简洁、易读,并提供了
更好的语法支持。
注意:Lambda表达式可以根据需要进行简化,例如省略参数类型、省略小括号,或
使用单个参数的情况下甚至省略参数完全。这使得Lambda表达式更加灵活和方便。
注意:虽然lambda表达式在C#中的发音通常为“拉姆达”或“兰布达”,但不同地区或
个人可能有差异。这只是一种常见的发音习惯,可以根据个人喜好选择。
问:为什么叫Lambda表达式?
答:lambda表达式名字的来源可以追溯到数学和计算机科学中的lambda演算
(lambda calculus)。
lambda演算是一种由数学家阿隆佐·邱奇(Alonzo Church)在上世纪30年代提出的
形式化计算模型。它基于一种简洁的函数定义和应用方法,用于描述计算和抽象概念。
lambda演算在计算机科学中具有重要的理论和实际应用,包括函数式编程语言的设计和
函数式编程范式的研究。
在编程语言中,lambda表达式则是一种函数定义和传递的简洁语法形式,受到了
lambda演算的影响。C#中的设计者在引入lambda表达式时,选择了"lambda"这个术语
来表示这种匿名函数的语法形式。
“lambda"一词的来历可以追溯到希腊字母“λambda”,它是lambda演算的标记符号。
为了表示将数学中的lambda演算概念引入到编程语言中,C#语言中的lambda表达式得
名为"lambda”。
通过使用lambda表达式,开发人员能够以简洁而强大的方式定义和传递功能,使
代码更易读、更紧凑。这种命名方式旨在强调C#中lambda表达式的理论和概念背景。
2、lambda表达式的例子:
(1)无参无返回值
public delegate void MyDelegate();//仍然要声明类型
MyDelegate md = () => Console.WriteLine("无参数无返回值情况");
md();
上面仍然要声明类型,当然也可使用系统自带的通用委托类型
Action f = () => Console.WriteLine("无参无返回值情况");
f();
上面连声明都省略了。Action是一个通用委托类型,表示一个无返回值且可接受零个或
多个参数的方法。它是一个用于执行无返回结果的操作的泛型委托。
(2)有参但无返回值
public delegate void MyDelegate(int n);//仍然要声明类型
MyDelegate md = (m) => Console.WriteLine($"有参无返回值{m}");;
md(23);
lambda参数类型可以自行推断,故m无须写明int。但自行推断不出,将出错。
因此并不强行要求写数据类型,只是根据lambda精简原则,一般不写。
同时只有一个参数时,可以不用(),简言之,()或参数必须要写一个。
(3)有参有返回值
public delegate int AddDelegate(int n1, int n2, int n3);
AddDelegate md = (a, b, c) => a + b + c; ;
Console.WriteLine(md(3, 3, 3));
上面返回值省略return:
AddDelegate md = (a, b, c) => { return a + b + c; };
注意:lambda表达式如果只包含单个表达式(也称为单行表达式),则可以省略return关
键字,并且表达式的结果将作为返回值。这是因为编译器将推断该表达式的结果为lambda
表达式的返回值。
对于多个表达式的lambda表达式,或者需要在代码块内执行额外的逻辑操作时,应该
显式使用return关键字指定返回值。
有返回值的还可使用系统定义的类型Func
Func f = (a, b, c) => a + b + c; ;
Console.WriteLine(f(1, 2, 3));//6
1、Action无返回值泛型委托
Action是一个泛型委托类型,用于表示不返回值的方法。它可以接受零个或多个参数,
并且没有返回类型。`Action`委托通常用于执行某种操作而不返回结果。
public delegate void Action();
public delegate void Action(T obj);
public delegate void Action(T1 arg1, T2 arg2);
// ... 可以有更多的参数类型重载
上面系统已经定义好了这个委托,因此这个无返回值的委托类型可以直接使用。
Action委托可以具有不同数量的参数,例如无参数的情况(`Action`)、一个参数
的情况(`Action
(1)无参数的情况:
Action greet = () => Console.WriteLine("Hello!");//a
greet(); // 输出 "Hello!"
注意上面a处lambda主体原为{Console.WriteLine("Hello!");};因为只有一个语句
可以省略{},剩下就有两个分号;;可以只写一个分号,当然两个分号也不会错,只不过
这时,第一个分号直接表示该语句结束了。第二个分别是一个空语句,不影响程序的流
程,故不会报错。
Action greet = () => Console.WriteLine("Hello!");;
(2)一个参数的情况:
Action printMessage = (message) => Console.WriteLine(message);
printMessage("Welcome!"); // 输出 "Welcome!"
(3)两个参数的情况:
Action calculateSum = (a, b) => {
int sum = a + b;
Console.WriteLine("Sum: " + sum);
};
calculateSum(3, 4); // 输出 "Sum: 7"
Action委托不返回值,因此它在需要执行某种操作而不需要返回结果的情况下非常
有用。如果你需要有返回值的委托类型,可以使用带有返回类型的Func委托类型。
总结:Action是一个非泛型的委托版本.
Action
问:上面lambda表达式是如何推断出参数的类型的?
答:根据Action
2、自定义泛型委托
泛型委托是一种能够适应不同参数类型和返回类型的委托。它允许你在定义委托时
不指定具体的参数类型和返回类型,而是使用泛型类型参数,从而实现更灵活和通用的
委托定义。
泛型委托的定义方式与普通委托略有不同。以下是泛型委托的语法:
public delegate Z GenericDelegate(X arg1, Y arg2);
上面GenericDelegate是一个泛型委托类型,它有两个类型参数:T和TResult。T表
示输入参数的类型,而TResult表示返回值的类型。你可以根据具体的需求来替换这些
类型参数。例:
public delegate TResult MyDelegate(T1 x, T2 y);
public static int AddInt(int a, int b)
{ return a + b; }
public static double MutiDouble(double a, double b)
{ return a * b; }
public static void Main()
{
MyDelegate md1 = new MyDelegate(AddInt);
Console.WriteLine(md1(1, 2));
MyDelegate md2 = new MyDelegate(MutiDouble);
Console.WriteLine(md2(1.3, 2));
Console.ReadKey();
}
这样在不改变委托定义的情况下,重复使用这些委托来执行不同的方法。泛型委托
能够提供更大的灵活性和重用性,因为它们可以适用于不同类型的参数和返回类型。这
使得它们在需要定义通用的回调或事件处理逻辑时非常有用。
3、协变与逆变复习
协变(covariance)和逆变(contravariance)通常与委托(delegate)和泛型接口
(generic interface)相关联。
(1)协变(Covariance):
协变允许某个类型参数在派生类中进行隐式向上转换。
通常我们使用`out`关键字来标识协变。考虑一个经典的示例,`IEnumerable` 接口是一个
泛型接口,定义了一个返回 `IEnumerator` 对象的方法 `GetEnumerator()`:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
在这个接口中,类型参数 `T` 被标记为协变(使用了 `out` 关键字)。这意味着可
以在使用时将接口类型参数视为它的派生类型。例,假设有一个名为 `Animal` 的基类和
一个派生类 `Cat`:
class Animal { }
class Cat : Animal { }
那么我们可以将 `IEnumerable
IEnumerable cats = new List();
IEnumerable animals = cats;
这是因为在协变中,我们可以将泛型类型参数向上转换为其基类或实现的接口。
(2)逆变(Contravariance):
逆变允许某个类型参数在派生类中进行隐式向下转换。
通常我们使用`in`关键字来标识逆变。考虑一个经典的示例,`IComparer` 接口是一个泛
型接口,定义了一个比较两个对象的方法 `Compare()`:
public interface IComparer
{
int Compare(T x, T y);
}
在这个接口中,类型参数 `T` 被标记为逆变(使用了 `in` 关键字)。这意味着可以
在使用时将接口类型参数视为它的基类类型。例如,假设有一个名为 `Animal` 的基类和
一个派生类 `Cat`:
class Animal { }
class Cat : Animal { }
那么我们可以将 `IComparer
IComparer animalComparer = new AnimalComparer();
IComparer catComparer = animalComparer;
这是因为在逆变中,我们可以将泛型类型参数向下转换为它的派生类型。
协变和逆变可以使代码更加灵活,并提供更好的多态性。通过使用协变和逆变,可以
以更抽象的方式处理类型,并在保持类型安全性的同时实现更高的代码复用性。
4、为什么要用in和out?
使用`in`关键字在泛型接口中用于指定逆变性的主要目的是在方法参数中获得更大的
灵活性。尽管逆变性在某些情况下可能看起来没有直接的用处,但它在特定的场景中确实
有其优势。主要有以下几种情况适合使用`in`关键字:
(1)参数只用于输入:
逆变性在意味着方法只接受类型`T`作为输入参数,而不是输出参数。在这种情况下,
`in`关键字可以让你明确表示该参数仅用于输入,并且不允许从中读取值。这样可以提高
代码的可读性和安全性。
(2)参数只用于方法内部操作:
如果方法只需要通过指定类型`T`来实现内部操作,而不需要从中读取值,那么`in`
关键字可以告诉调用者该参数只用于内部处理,不会被转发或修改。
(3)避免对原始对象的意外修改:
逆变性可以确保泛型接口中的泛型参数不会被修改。这样可以防止在方法中对原始对
象进行不必要的更改,从而提高代码的安全性。
虽然使用`in`关键字时可能有一些限制,但它可以在某些特定的场景中提供更好的接
口设计和代码安全性。逆变性的使用要根据具体情况来判断,根据实际需求来决定是否使
用该特性。
总结:使用in关键字来限制只输入并确保安全是in关键字的主要目的之一。它提供了
一种在泛型接口中定义逆变性的方式,以实现更好的类型安全性和代码可读性。
`out`关键字与`in`关键字相反,用于泛型接口或委托中的类型参数,表示该类型参
数只能用作输出(方法返回值),而不能用作输入。
使用`out`关键字的目的是提供协变性,即允许将子类对象赋值给父类对象(或将派
生类型转换为基类型)。这样可以增加方法或接口的灵活性,并使得方法返回的类型与
参数类型的关系更加符合预期。主要有以下几种情况适合使用`out`关键字:
(1)方法返回值:
使用`out`关键字指定方法的返回值类型,并确保返回的类型是派生类型(子类)的
实例。这样可以使调用者能够将返回值直接赋值给父类对象,而无需进行类型转换。
(2)数组和集合:
使用`out`关键字定义泛型数组或集合的类型参数,以便在添加元素时允许使用派生
类型。
(3)委托:
使用`out`关键字定义委托中的返回值类型,以便在委托调用时返回派生类型的实例。
使用`out`关键字不仅提供了协变性,还可以提供编译时的类型检查,以确保类型的
兼容性。这样可以降低类型转换错误的风险,并提高代码的可靠性和安全性。
总结:`out`关键字用于指定泛型类型参数只能用作输出,提供协变性并允许将派生
类型赋值给基类型。它主要用于方法返回值、数组和集合、委托中。通过使用`out`关键
字,可以增加代码的灵活性和可读性,并提供类型安全性和编译时的类型检查。
重要:协变并不一定需要使用 out 关键字,只要存在将派生类型转换为基类型的能
力,都可以认为是协变。
注意,泛型类型参数中的out和in修饰符表示协变和逆变,且在<>内使用。而方法参
数中的out修饰符表示参数是一个输出参数。它们在语义上有所不同,不能直接混淆使用。
协变和逆变的规则不仅仅与out和in修饰符相关,还与泛型类型参数的位置和约束有关。
out和in只是用于提醒开发人员类型参数的协变和逆变能力。
归纳:out有两个作用。
(1)把值从方法中带回来。
private static void Main(string[] args)
{
AddNum(1, 2, out int r);
Console.WriteLine(r);
Console.ReadKey();
}
private static void AddNum(int n1, int n2, out int result)
{ result = n1 + n2; }
上面out中通过r->result,不需要return,直接把值给了主函数中的r。注意r的作用
整个主函数体内,尽管它只是在Addnum函数括号内定义。
(2)协变中out指明只能由自己或派生类转向基类
internal class Program
{
private static void Main(string[] args)
{
Dog dog = new Dog();
Animal animal = dog;//协变 "和协"变化 子->父
List doglist = new List();
//List animallist = doglist;//错误
List animals = doglist.Select(d => (Animal)d).ToList();//逐个转基类
//public delegate void Action(T obj)
//逆变 in 只能输入,只能被使用,不能作为返回值
Action actionAnimal = a => Console.WriteLine($"{a}叫");
Action actionDog = actionAnimal;//逆变 父->子
actionDog(dog);
Console.ReadKey();
}
}
internal class Animal{ }
internal class Dog : Animal{ }
问:上面为什么要ToList?
答:同where一样Select返回值是IEnumerable
类型的序列。它用于表示一个能够按顺序访问并枚举 TResult 类型元素的集合。
IEnumerable
接口。它提供了枚举集合元素的能力,但没有直接的索引访问或修改元素的方法。
IEnumerable
了 IEnumerator
可用于按顺序遍历 IEnumerable
IEnumerable
它是 LINQ 查询和操作的基础接口,可以方便地进行迭代、筛选、转换等操作并处理结果。
注意,虽然 IEnumerable
体的逻辑。具体的序列可以是数组、列表、集合、查询结果等。在实际使用中,你可以通
过使用各种数据结构实现 IEnumerable
常用的方法和属性:
GetEnumerator():
该方法返回一个实现了 IEnumerator
Current:
IEnumerator
MoveNext():
IEnumerator
下一个元素时,返回 true;当已经到达序列的末尾时,返回 false。
Reset():
IEnumerator
var list = new List { 1, 2, 3, 4 };
var e = list as IEnumerable;
var t = e.GetEnumerator();
while (t.MoveNext())//最末时为false,退出循环
{
Console.WriteLine(t.Current);
}
t.Reset();//重置循环,位于开始处
Console.WriteLine(t.MoveNext());//true
问:上面为什么用var?
答:使用 var 关键字来声明变量是一种更简洁的语法,它能够根据变量的初始化表达
式自动推断变量的类型。使用 var 不会对性能产生直接的影响,因为在编译时会将
var 推断为适当的类型,并生成相同的 IL(Intermediate Language)代码。
var 只是一种编译器语法糖,提供了更方便的变量声明方式,但变量的类型仍然
是静态的,并且一旦确定就不能更改。
使用 var 的好处是可以使代码更加简洁和可读性更高,同时减少了重复的类型声
明,尤其是在使用泛型集合类和复杂的匿名类型时。它强调了代码的意图而非细节,
使得代码更易于理解和维护。
强调:它不会影响性能。只是在编译时比显式声明多了一步推断。但在可简化写
一些复杂的类型。例如上面的列表、枚举等。
ToList():
该方法是 System.Linq 命名空间中的扩展方法,用于将 IEnumerable
List
Count():
该方法是 System.Linq 命名空间中的扩展方法,用于计算序列中的元素个数,并返回
一个整数。
Any():
该方法是 System.Linq 命名空间中的扩展方法,用于判断序列是否包含任何元素。如
果序列中有元素存在,返回 true;否则返回 false。
FirstOrDefault():
该方法是 System.Linq 命名空间中的扩展方法,用于获取序列的第一个元素,如果序
列为空,则返回默认值。
Where():
该方法是 System.Linq 命名空间中的扩展方法,用于根据指定的条件筛选序列中的元
素,返回一个新的序列。
var list = new List { 1, 2, 3, 4, 5, 6 };
bool b1 = list.Any(x => x > 3);//是否有大于3
Console.WriteLine(b1);
bool b2 = list.Any(x => x % 2 == 0);//是否有偶数
Console.WriteLine(b2);
int n1 = list.FirstOrDefault();
Console.WriteLine(n1);//1
List list2 = new List();
int? n2 = list2.FirstOrDefault();
Console.WriteLine(n2 == null);//false list默认值0
List list3 = new List();
string n3 = list3.FirstOrDefault();
Console.WriteLine(n3 == null);//true
List list4 = list.Where(x => x % 2 == 0).ToList();//取偶数组成列表
5、协变与逆变案例
模拟了水果从小贩那里卖出到消费者手中的过程。
internal class Program
{
public static void Main()
{
IVendor fruitVendor = new FruitVendor();
Fruit item = fruitVendor.GetItem();//协变
Console.WriteLine(item.GetType().Name);
IConsumer orangeConsumer = new FruitVendor();//逆变
orangeConsumer.ConsumeItem(new Orange());
Console.ReadKey();
}
}
public interface IVendor
{ T GetItem(); }
public interface IConsumer
{ void ConsumeItem(T item); }
public abstract class Fruit
{ }
public class Orange : Fruit
{ }
public class Apple : Fruit
{ }
public class FruitVendor : IVendor, IConsumer
{
public Fruit GetItem()
{
Console.WriteLine("Getting a fruit from the vendor");
return new Apple();
}
public void ConsumeItem(Orange item)
{
Console.WriteLine("Consuming an orange");
}
}
协变是通过IVendor
协变的。它允许 GetItem() 方法的返回类型可以是 T 或者 T 的子类型。所以,在
FruitVendor 类中的 GetItem() 方法返回了一个 Fruit 对象,即使 Fruit 的子类
Apple 也是允许的。
将 FruitVendor 对象赋值给 IConsumer
实现了逆变。逆变意味着我们可以将一个父类型的实例赋值给接口类型的变量,以实现
更高层次的抽象。在这种情况下,FruitVendor 是 IConsumer
并且 ConsumeItem 方法接受 Orange 类型的参数。通过使用逆变,我们可以将
FruitVendor 实例赋值给 IConsumer
我们可以通过 orangeConsumer 来调用接口定义中的方法。
6、谓词
谓词(Predicate)是一个委托类型,用于表示一个方法或 Lambda 表达式,该方法或
表达式接受一个参数并返回一个布尔值。
谓词通常用于对集合中的元素进行筛选、过滤或匹配。它可以用于各种情况,如查找满
足特定条件的元素、判断元素是否符合某个条件等。
谓词涉及到对集合进行筛选、过滤或匹配,常见谓词如下:
(1)基本谓词:
Predicate
Func
(2)Linq 方法中的谓词:
Where:用于从集合中筛选满足条件的元素。
All:用于判断集合中的所有元素是否都满足指定的条件。
Any:用于判断集合中是否存在任何一个元素满足指定的条件。
First / FirstOrDefault / Last / LastOrDefault:用于获取满足条件的第一个或最
后一个元素。
Single / SingleOrDefault:用于获取满足条件的单个元素,如果有多个或没有满足
条件的元素则抛出异常或返回默认值。
SkipWhile / TakeWhile:用于跳过或获取从满足条件的元素开始之后的所有元素。
OrderBy / OrderByDescending:用于按照指定的条件对集合进行升序或降序排序。
(3)委托方法参数的谓词:
Find / FindAll:用于查找满足条件的元素或元素集合。
Exists:用于判断集合中是否存在满足指定条件的元素。
问:First与FirstOrDefault的区别是什么?
答:两者都是用于从序列中选择第一个匹配条件的元素。它们之间的区别在于当没有找到
匹配的元素时的行为。
First() 方法在找不到匹配的元素时会抛出异常,而 FirstOrDefault() 方法在找不
到匹配的元素时会返回一个指定的默认值。
var list = new List { 1, 2, 3, 4, 5, };
int n1 = list.First(x => x > 5);//空值,异常
Console.WriteLine(n1);
int n2 = list.FirstOrDefault(x => x > 5);//空时默认0
Console.WriteLine(n2);
int n3 = list.Any(x => x > 5) ? list.FirstOrDefault(x => x > 5) : -6;
Console.WriteLine(n3);//-6
Last与LastOrDefault类似。
问:single比first与last更严格?
答:是的,single要求返回单个元素的序列。如果返回满足条件的多个元素或则返回0个元
素(即空)时,就会引发System.InvalidOperationException 异常。
如果single因为空而引发异常可以用singleordefault来完善,但若是single因返回多
个元素时,则需要结合firstordefault或lastordefault等一些方法或技巧来进行处理。
string[] fruits = { "apple", "banana", "orange", "grape", "kiwi", "kiwi" };
var s1 = fruits.Single(x => x == "apple");
Console.WriteLine(s1);
//var s2 = fruits.Single(x => x == "aaa");//无元素,异常
//var s3 = fruits.Single(x => x == "kiwi");//多元素,异常
var s4 = fruits.SingleOrDefault(x => x == "aaa");
Console.WriteLine(s4 ?? "空元素");
string[] s5 = fruits.Where(x => x == "kiwi").ToArray();
if (s5.Length > 0) { Console.WriteLine(fruits.First(x => x == "kiwi")); }
string s6 = fruits.DefaultIfEmpty("没有匹配的水果").FirstOrDefault(x => x == "kiwi");
Console.WriteLine(s6);
上面可以看到判断single多元素或0元素时,比较麻烦。
举例:
List list = new List() { 3, 2, 5, 9, 4, 6, 8, 1, 2 };
List n1 = list.Skip(3).ToList();//3个元素后的所有
Console.WriteLine(string.Join("_", n1.ToArray()));//9_4_6_8_1_2
List n2 = list.SkipWhile(x => x > 2).ToList();
Console.WriteLine(string.Join("_", n2.ToArray()));//2_5_9_4_6_8_1_2
List n3 = list.TakeWhile(x => x > 2).ToList();
Console.WriteLine(string.Join("_", n3.ToArray()));//3
int n4 = list.Find(x => x > 6);
Console.WriteLine(n4);//9
List n5 = list.FindAll(x => x > 6);
Console.WriteLine(string.Join("_", n5.ToArray()));//9_8
bool b = list.Exists(x => x > 10);
Console.WriteLine(b);//false
问:TakeWhile与Where的什么区别?
答:两者相似,但有区别:
TakeWhile 方法用于从序列的开头获取满足指定条件的元素,直到找到第一个不满足条
件的元素为止,并返回这些满足条件的元素。
Where 方法用于筛选序列中满足指定条件的所有元素,并返回一个新的序列。
var numbers = new List { 1, 2, 3, 4, 5, 6 };
var result1 = numbers.TakeWhile(x => x <= 3);
Console.WriteLine(string.Join("_", result1.ToArray()));//1_2_3
var result2 = numbers.Where(x => x <= 3);
Console.WriteLine(string.Join("_", result2.ToArray()));//1_2_3
var numbers2 = new List { 1, 2, 3, 4, 5, 0, 6 };
result1 = numbers2.TakeWhile(x => x <= 3);
Console.WriteLine(string.Join("_", result1.ToArray()));//1_2_3
result2 = numbers2.Where(x => x <= 3);
Console.WriteLine(string.Join("_", result2.ToArray()));//1_2_3_0
问:takewhile,skipwhile,where的区别?
答:where是从全部序列中进行筛选,每一个都参加筛选,满足条件则进入返回序列。
skipwhile跳过选后面,从头开始满足条件跳过,直到找到第一个不满足的,从它
开始后面所有的元素(无论后面的是否满足条件)。
如果第一个就不满足,从它开始后面所有,即整个序列都返回。
如果全部都不满足,全都跳过了,后面再无元素可先,返回空序列。
takewhile满足先前面,从头开始只要满足就进入返回序列,直到不满足则中止。
如果第一个元素都不满足,则直接终止,返回空序列。
如果全部满足,到了结束,自动返回全部序列。
总结:skipwhile与takewhile跟第一个满足或不满足有关。while则全部都得筛选。
问:为什么叫谓词?而不叫其它的?
答:谓词的名字来源于数学中的谓词逻辑,谓词逻辑是关于命题的逻辑系统,在这个系统中,
命题通过谓词表达式来定义和判断。比如离散数学。在C#中,谓词扮演着类似的角色,用于
表示和判断给定条件。比如常用于定义条件的委托,它在LINQ查询中经常被使用,用于筛选
符合条件的元素。谓词的名字来源于数学中的谓词逻辑,表示一种条件表达式的判断方式。
所以常看到参数Func
问:谓词where与select有什么区别?
答:where是从原集合元素中进行筛选,结果仍然保持着原元素的样貌。比如筛选大于6的,
那么,得出的元素,没发生转换(或大或小)
Select是对原集合元素的转换,结果很可能与原元素样貌不一样。比如将原元素翻倍,
那么原来的元素就变成了2倍了,发生转换。
两者可以结合使用。(取小于等于5长度的元素进行大写)
List s = new List() { "apple", "banana", "cherry", "date" };
List ss = s.Where(x => x.Length <= 5).Select(x => x.ToUpper()).ToList();
Console.WriteLine(string.Join("-", ss.ToArray()));
7、Predicate
Predicate
用于判断一个值是否满足指定的条件。是系统预定义的委托,定义如下:
public delegate bool Predicate(T obj);
它接受一个泛型参数 `T`,表示要判断的值的类型。该委托类型的方法签名要求接受一
个类型为 `T` 的参数,返回一个布尔值。当参数满足指定条件时,方法返回 `true`;否
则,返回 `false`。
Predicate
Find :用于查找满足条件的单个元素。
FindAll :用于查找满足条件的所有元素。
Exists :用于判断集合中是否存在满足指定条件的元素。
RemoveAll :用于删除集合中满足条件的所有元素。
private static void Main(string[] args)
{
List words = new List { "apple", "banana", "cherry", "date" };
List r = words.FindAll(ContainS);//a
foreach (string s in r) { Console.WriteLine(s); }
Console.ReadKey();
}
private static bool ContainS(string s)//含a的方法
{ return s.Contains("a"); }
a处可改为List
8、介绍一下Func
在C#中,`Func
受零个或多个参数的方法。`T`表示类型参数,可以用于指定参数的类型和返回值的类型。
`Func
表示方法的输入参数类型。最多可以有16个参数类型,分别用T1、T2、...、T16表示。
(1)无参有返回值的情况:
Func getName = () => "John Smith";
string name = getName(); // 返回 "John Smith"
Func
(2)有参有返回值的情况:
Func add = (a, b) => a + b;
int result = add(5, 3); // 返回 8
Func
(3)多个参数的情况(最多16个参数):
Func isSumGreaterThanTen = (a, b, c) => (a + b + c) > 10;
bool isGreater = isSumGreaterThanTen(3, 4, 5); // 返回 true
Func
通过使用Func
非常方便地使用lambda表达式来实现这些方法的逻辑。注意Action无返回值。
问:Func
答:Func
只能用于方法的参数列表中,不能应用于委托类型的泛型参数列表。
Func
它的泛型参数列表中的每个参数都对应方法的参数类型,而最后一个泛型参数表示方法
的返回类型。因此params在这里并不适用。
如果你想要定义一个具有可变长度参数的委托类型,你可以使用delegate关键字来
自定义委托类型,并在其参数列表中使用`params`关键字:
delegate int CustomDelegate(params int[] numbers);
CustomDelegate接受可变长度的整数参数并返回一个整数。
注意:前面几个输入参数,不能加out用来带回返回值。违反预定义委托的限定:前
面参数须要具体值传进去,而不是用out把委托中的赋值带出来。
9、实战
例:{1,2,3,4,5,6,7,89,10,11,12,13,14,15}找出大于6的数字
List list = new List { 1, 2, 3, 4, 5, 6, 7, 89, 10, 11, 12, 13, 14, 15 };
List n = list.Where(x => x > 6).ToList();
Console.WriteLine(string.Join("-", n.ToArray()));
List
第一个参数是集合中的元素类型 T,第二个参数是该元素在集合中的索引。根据元素的
值和索引,可以做复杂的事。
List numbers = new List { 1, 2, 6, 4, 5 };
IEnumerable n2 = numbers.Where((x, index) => x > index);
Console.WriteLine(string.Join("-", n2.ToArray()));
索引从头是以0开始的。
1、多播委托
delegate void ProcessWordDelegate(string s)
ProcessWordDelegate d = new ProcessWordDelegate(SayHello)+new ProcessWordDelegate(ToLower)
多播委托如何处理返回值?
委托绑定多个方法后,其中一个方法执行发生异常后面的方法还会继续执行吗?不会!
一个重要的方法GetInvocationList();//返回一个Delegate[]类型。
Delegate类是一个抽象类,是所有委托的父类。
组合的委托必须是同一个类型,相当于创建了一个按照组合的顺序依次调用的新委托
对象。
委托的组合一般是给事件用的,用普通的委托的时候很少用
从多播委托中删除一个不存在的普通委托是安全的,不会导致异常。当一个多播委托
只剩下一个委托时,尝试将其从多播委托中删除会引发异常。在删除多播委托中
唯一的委托时,需要进行一些额外的逻辑判断,以避免异常的发生。
问:委托是类吗?
答:本质上它并不是一个类。
当声明一个委托时,编译器会生成一个新的委托类型,该类型是派生自
System.MulticastDelegate 类并被标记为密封的。这意味着我们不能从委托类型派生
或继承,我们只能使用生成的委托类型作为委托变量的类型。
尽管在反编译工具中可以看到委托类型被显示为 class sealed,但实际上这并不
是字面意义上的类继承和密封。这是一个反编译工具用于表示委托类型的方式,以强调
委托被设计为不可继承的特性。
尽管委托类型不是通过 class 关键字定义的类,但我们可以将其类比为密封类,因
为委托类型在行为上类似于密封类。
问:委托的定义有几种?
答:有四种:
(1)象类一样:
delegate void MyDelegate(string message);
MyDelegate delegateInstance = new MyDelegate(MethodName);// 委托实例的创建方式
(2)使用匿名方法创建委托
delegate void MyDelegate(string message);
MyDelegate delegateInstance = delegate (string message)// 匿名方法创建委托实例
{
Console.WriteLine(message);
};
(3)使用 Lambda 表达式创建委托
delegate void MyDelegate(string message);
MyDelegate delegateInstance = (message) =>
{
Console.WriteLine(message);
};// 使用 Lambda 表达式创建委托实例
(4)使用方法组创建委托
delegate void MyDelegate(string message);
MyDelegate delegateInstance = MethodName;// 方法组创建委托实例
哪种形式取决程序及你的喜好。
Mydelegate m = new Mydelegate(M1);
m = (Mydelegate)(Delegate.Combine(m, (Mydelegate)M2));
m = (Mydelegate)(Delegate.Combine(m, new Mydelegate(M3)));
m = (Mydelegate)(Delegate.Remove(m, new Mydelegate(M4)));
m = (Mydelegate)(Delegate.Remove(m, new Mydelegate(M2)));
m("小明");
为何(Mydelegate)M2不写成M2?
虽然 M2 可能具有与 Mydelegate 委托类型相匹配的签名,但编译器并不知道这一点。
因此,为了确保类型兼容性,需要在 M2 前面添加 (Mydelegate) 强制转换来明确指示编
译器将其视为 Mydelegate 类型。
同样Combine后返回的是Delegate类型,需要显式转为MyDelegate。
2、问:MultiCastDelegate上下类的情况介绍下?
答:Delegate是基类,MulticastDelegate是派生类,而普通委托类型(Action、Func等)
是根据 MulticastDelegate 类定义的具体委托类型。
Delegate 是一个抽象基类,用作所有委托类型的基础。Delegate 类定义了委托的
核心功能和行为,例如 Target、Method 和 Invoke。
MulticastDelegate 类则是 Delegate 类的派生类。它扩展了 Delegate 类的功能,
允许表示包含多个方法的委托。MulticastDelegate 类提供了合并(Combine)多个委
托实例以及移除(Remove)一个委托实例等操作。
普通的委托类型,例如 Action 或 Func,是派生自 MulticastDelegate 类的具体
委托类型。这些具体委托类型被用来表示特定的方法签名,如不带参数且有返回值的方
法 (Func) 或不带参数且没有返回值的方法 (Action)。
MulticastDelegate 类在以下几个方面扩展了 Delegate 类的功能:
(1)合并多个委托实例:
MulticastDelegate 提供了 Combine 方法,用于合并多个委托实例。通过调用
Combine 方法,可以将多个委托实例合并为一个新的 MulticastDelegate 实例,它将
依次调用每个委托实例的方法。实则为+
(2)移除委托实例:
MulticastDelegate 提供了 Remove 方法,用于从 MulticastDelegate 实例中移
除单个委托实例。这样可以创建一个新的 MulticastDelegate 实例,它不包含被移除
的委托实例。实则为-
(3)获取委托链上所有委托实例:
通过 GetInvocationList 方法,MulticastDelegate 类允许获取委托链上的所有委
托实例。该方法返回一个委托实例数组,可以对每个委托实例执行操作。
Action a1 = () => Console.WriteLine("1");
Action a2 = () => Console.WriteLine("2");
Action a3 = () => Console.WriteLine("3");
Action a4 = a1 + a2 + a3;
a4 -= a2;
Delegate[] dd = a4.GetInvocationList();
foreach (Action d in dd)
{
d();
}
多播委托是一个链表,其顺序是先进先出(First-In-First-Out)的顺序。
提示:GetInvocationList() 方法用于在C#中检索订阅事件的委托对象数组。可以在事件
上调用 GetInvocationList() 方法,它将返回当前订阅该事件的委托数组。数组中
的每个元素都是一个委托对象,表示在引发事件时将执行的方法。
注意:GetInvocationList() 方法会将链表形式的多播委托转换为委托数组形式。它返回
一个委托数组,其中每个元素表示一个目标方法的委托。该委托数组是在获取调用列
表时创建的,将多播委托中的所有目标方法以数组的形式表示。
数组dd中的委托是不能直接使用的,它是Delegate抽象类,需要显式转为普通委托,
例子中没有显式转化,但在foreach中用action进行转化了。实际中,若foreach没有
转换,需要人为显式进行转化,就象类一样前面用括号加普通委托来转化。甚至可以
直接用as来进行转化。
问:有返回值时,调用多播委托的结果是第一个还是最后一个?
答:最后一个. 若要得到每个委托返回值用GetInvocationList,注意类型转换。
public delegate int Mydelegate();
private static void Main(string[] args)
{
Mydelegate m = M1;
m += M2;
m = (Mydelegate)(Delegate.Combine(m, (Mydelegate)M3, (Mydelegate)M4));
Console.WriteLine(m());//4
Delegate[] ds = m.GetInvocationList();
foreach (Delegate d in ds)
{
Mydelegate mm = (Mydelegate)d;
Console.WriteLine(mm());
}
Console.ReadKey();
}
private static int M1()
{ return 1; }
private static int M2()
{ return 2; }
private static int M3()
{ return 3; }
private static int M4()
{ return 4; }
问:为什么叫多播委托?
答:委托(Delegate)可以被看作是一个对方法的引用或指针。多播委托可以看作是一
个指向多个方法的指针列表。多播委托是一种特殊类型的委托,它能够同时持有和调用
多个方法。它可以将多个方法绑定到同一个委托实例上,形成一个委托链(Delegate
Chain),使得在调用委托时会按照添加的顺序依次调用委托链中的每个方法。
实际上,多播委托内部维护了一个委托实例列表,这些委托实例都是指向不同方法
的指针。当多播委托被调用时,它会遍历该列表,并依次调用每个委托实例所指向的方
法。
问:多播委托与普通委托的区别?
答:(1)存储方式:
多播委托使用一个字段来存储多个方法引用,而普通委托只能存储一个方法引用。
(2)绑定和解绑操作:
多播委托使用`+=`运算符来绑定方法,使用`-=`运算符来解绑方法。每次绑定或解
绑操作都会修改委托对象本身。普通委托只能使用赋值操作符`=`来绑定方法,且
每次绑定操作都会创建一个新的委托对象。
(3)执行顺序:
多播委托中存储的方法引用按照绑定的顺序依次执行,而普通委托只会执行最后绑
定的方法。
(4)返回值:
多播委托的返回值只能为`void`,而普通委托可以定义任意返回类型。
如果需要同时触发多个方法,可以使用多播委托。如果只需绑定单个方法或动态替换方法,
可以使用普通委托。
问:delegate与Delegate有什么区别?
答:delegate是关键字,主要用于定义及实例化(匿名)
delegate void MyDelegate(int x);
MyDelegate myDelegate = delegate (int x)
{
Console.WriteLine("匿名函数被调用,参数值为: " + x);
};
Delegate是类,是C#中表示委托的抽象基类。它是所有委托类型的基类,定义了
委托的通用行为和特性。有下面属性方法:
Method:获取委托所绑定的方法的信息(MethodInfo对象)。
Target:获取委托所绑定的方法所属的对象实例(如果有的话)。
Combine(Delegate, Delegate):静态方法,将两个委托合并为一个新的委托。合
并后的委托将按照添加的顺序执行。
Remove(Delegate, Delegate):静态方法,从一个委托中移除另一个委托。移除
后的委托将按照添加的顺序执行。
DynamicInvoke(params object[]):动态方法,允许在运行时以对象数组的形式动
态传递参数并调用委托引用的方法。
Clone():创建委托的浅拷贝副本。
问:委托实例化时能否省略作为参数的方法?如下面:
Mydelegate m = new Mydelegate();
答:不能!
委托是一种类型,它定义了一个方法列表,可以将这些方法作为参数传递给其他
方法。如果省略了方法参数,编译器将无法识别方法的签名,从而导致编译错误。
委托实例化时,可以省略前面的一些参数,这些参数将使用默认值,但是后面的
参数是必需的,因为它们将被包含在委托对象中,并且必须在委托实例化时指定。
3、委托的不可变性
委托具有类似于string一样的不可变性。
建议使用的时候尽量少定义自己的委托,使用系统中已经有的委托。减少程序集中定
义的类型的个数。
委托的不可变性指的是委托实例在创建之后无法更改其所引用的方法列表。一旦委托
实例被创建,就无法替换或修改其方法引用。
当你为委托赋值一个方法或者使用 += 运算符添加一个方法到委托中时,实际上是创
建了一个新的委托实例,该实例引用了新的方法列表。原始的委托实例仍然存在,
但它不再引用新添加的方法。
委托的不可变性使得委托实例在多线程环境下是线程安全的,因为你无法在运行时更
改委托所引用的方法列表。一个线程在用委托,另一个在改变委托,后果将不一致
注意:委托变量(即引用委托的变量)本身是可变的,可以分配新的委托实例给它,
但是分配后的委托实例是不可变的。
1、事件就是委托的具体应用,下面用委托来模拟事件。
例1:播放音乐前加载歌词与背景,停止音乐前删除歌词与背景。
internal class Program
{
private static void Main(string[] args)
{
MusicPlay m = new MusicPlay();
m.AfterStarted = () =>
{
Console.WriteLine("显示歌词");
Console.WriteLine("显示背景");
};
m.BeforeStopped = () =>
{
Console.WriteLine("删除歌词");
Console.WriteLine("删除背景");
};
//m.BeforeStopped=null;//a
//m.AfterStarted=null;//b
m.StartPlay();//c
m.StopPlay();//d
Console.ReadKey();
}
}
internal class MusicPlay
{
public Action AfterStarted;//e
public Action BeforeStopped;//f
private void Play()
{ Console.WriteLine("播放音乐..."); }
public void StartPlay()
{
Play();
if (AfterStarted != null) { AfterStarted(); }
Thread.Sleep(2000);
}
public void StopPlay()
{
if (BeforeStopped != null) { BeforeStopped(); }
Console.WriteLine("停止音乐!!");
}
}
问题一:可以单独使用AfterStarted,BeforeStopped,在逻辑上不合理,这些事情只有
有一定条件才能执行,如播放前。但这两个委托为public,可随时触发,若改为
private时用户又无法设置。
问题二:在赋值后,又在a,b处单独设置为null,因为委托可以直接用等号赋值,这样就
把以前注册的方法直接覆盖掉了。
例2:用委托实现三连击弹出信息。新建一个用户控件:右击项目->添加->用户控件,
双击Button添加Button到用户控件上。
双击用户控制添加:
private int count = 0;
public Action TripleClick;//不但定义了委托,还声明了变量TripleClick
private void button1_Click(object sender, EventArgs e)
{
count++;
if (count >= 3)
{
if (TripleClick != null) TripleClick();
count = 0;
}
}
在form1中添加两个button用于设置委托,以便三击form1中的用户自定义控件不同效果。
private void button1_Click(object sender, EventArgs e)
{
userControl11.TripleClick = () => MessageBox.Show("罗刹海市");
}
private void button2_Click(object sender, EventArgs e)
{
userControl11.TripleClick = () => MessageBox.Show("颠倒歌");
}
改写上面为事件:
private int count = 0;
public event Action TripleClick;//前面多个event,其余与委托变量一样
private void button1_Click(object sender, EventArgs e)
{
count++;
if (count >= 3)
{
if (TripleClick != null) TripleClick();
count = 0;
}
}
由于事件不能用=只能用+=或-+,所以点击时也应更改:
private void button1_Click(object sender, EventArgs e)
{
//userControl11.TripleClick = () => MessageBox.Show("罗刹海市");
userControl11.TripleClick += () => MessageBox.Show("罗刹海市");
}
private void button2_Click(object sender, EventArgs e)
{
//userControl11.TripleClick = () => MessageBox.Show("颠倒歌");
userControl11.TripleClick += () => MessageBox.Show("颠倒歌");
}
上面就变也了多播委托,后面点击时会出现两个委托的对话框。
问:如何删除由匿名委托组成的多播委托中的第一个呢?
答:用GetInvocationList拆分原多播委托,再将除第一个外的所有再组成+=即新多播
2、观察:现在再把例1重新用事件来改写一下,来改变其中的两个问题:
internal class Program
{
private static void Main(string[] args)
{
MusicPlay m = new MusicPlay();
//m.AfterStarted = null;//不能直接赋值 a
m.AfterStarted += new Action(M_AfterStarted);//b
m.BeforeStopped += M_BeforeStopped;//c
m.StartPlay();//c
m.StopPlay();//d
//m.AfterStarted();//e
Console.ReadKey();
}
private static void M_BeforeStopped()
{ Console.WriteLine("删除歌词"); }
private static void M_AfterStarted()
{ Console.WriteLine("添加歌词"); }
}
internal class MusicPlay
{
public event Action AfterStarted;
public event Action BeforeStopped;
private void Play()
{ Console.WriteLine("播放音乐..."); }
public void StartPlay()
{
Play();
if (AfterStarted != null) { AfterStarted(); }
Thread.Sleep(2000);
}
public void StopPlay()
{
if (BeforeStopped != null) { BeforeStopped(); }
Console.WriteLine("停止音乐!!");
}
}
(1)事件不能直接赋值,a处为错误。
(2)用+=或-=可以赋值。且可以直接用方法,或new Action(方法名).
(3)不能直接调用事件,e处为错误。因为事件都是有触发条件的,仅在类内调用,
类外不能直接使用。
(4)事件的定义:就是委托声明变量前加event:
class MyClass
{
public event EventHandler MyEvent;
}
反编译上面:
事件AfterStarted被编译成两个公有方法和一私有字段。所以可以通过仅有字段在类
外调用,但却不能使用私有的委托字段。
3、理解关键
问:为什么事件不能直接用等号赋值,而是用+=或-=来赋值?
答:事件是一种特殊类型的委托,用于实现观察者模式,即当特定的事件发生时,允许
其他对象注册并接收通知。
事件的赋值操作需要通过使用`+=`和`-=`来添加或移除事件处理程序。这是由C#语
言设计决策所决定的,出于以下几个原因:
(1)安全性:
使用`+=`和`-=`操作符可以确保在事件赋值时,只能添加或移除特定的事件处理程
序。这在多播委托中是非常重要的,因为它可以有多个事件处理程序。如果可以使用赋
值操作符直接替换事件处理程序,那么可能会破坏事件的完整性和预期行为。
(2)语义清晰:
使用`+=`和`-=`操作符来赋值事件,可以很清晰地表达事件处理程序的添加和移除
操作。这使得代码更易读和易懂。
(3)统一性:
事件是一种特殊的成员,其语法规范与其他常规成员(字段、属性、方法)有所不
同。通过强制使用`+=`和`-=`操作符,可以保持一致性,并且符合C#的语法约定和惯例。
问:什么是观察者模式?
答:观察者模式(Observer Pattern)是一种软件设计模式,它定义了一种一对多的依
赖关系,使得多个观察者对象可以同时监听并接收到被观察者对象的状态变化通知。
下面用生活中的例子——报纸订阅与发送通知,说明观察者模式:
假设有一个报纸出版社(被观察者),它发布最新的报纸内容。有许多人订阅了这
份报纸(观察者)。当出版社发布新报纸时,订阅者们会收到通知,并可以获取到最新
的报纸内容。其中,出版社是被观察者,订阅者是观察者。
观察者模式的实现步骤如下:
(1)定义被观察者接口(出版社):
该接口包含添加观察者、移除观察者和通知观察者的方法。
(2)定义观察者接口(订阅者):
该接口包含接收通知并更新的方法。
(3)实现被观察者接口:
出版社类实现被观察者接口,并在其中维护一个观察者列表。
(4)实现观察者接口:
订阅者类实现观察者接口,并实现接收通知并更新的方法。
当出版社发布新报纸时,它会遍历观察者列表,并调用每个观察者的更新方法,
将最新的报纸内容传递给观察者。观察者收到通知后,就可以进行相应的操作,比如
读取最新报纸的内容。
这个例子中的报纸订阅与发送通知过程就是典型的观察者模式,被观察者(出版
社)与观察者(订阅者)之间建立了依赖关系。当被观察者状态变化时,观察者会接
收到通知并做出相应的响应。
观察者模式的优点是解耦了被观察者和观察者,使其彼此独立地进行扩展和变
化。这种模式在很多GUI框架、事件处理系统和发布订阅系统中都有广泛应用。
问:为什么事件不能单独调用?
答:事件本质上是一种特殊的委托,用于实现观察者模式。与普通的委托不同,事件具
有一些限制,其中之一是它们不能在外部直接调用。
事件被设计成只能由定义它的类内部触发,不能在外部直接调用。这是为了保护事
件的完整性和封装性,确保事件的触发是受控的,并且只能通过定义类内部的特定方法
来触发。这种限制有以下几个原因:
(1)封装性和安全性:
事件通常是作为类的一种重要成员,用于管理内部状态和操作。通过限制事件只能
在类内部触发,可以保护事件的封装性和防止不合理的外部访问和调用。这样可以确保
事件的触发与类的内部逻辑和状态同步,避免意外的行为和损害数据的一致性。
(2)一致性和可维护性:
事件的触发往往涉及到类内部的其他操作和逻辑。通过统一限制事件的触发方式,
可以使代码更一致、更易读、更易维护。这样开发人员在使用该类时,可以更好地了解
哪些操作会触发事件,方便进行相关处理。
(3)观察者模式的约定:
事件通常用于实现观察者模式,其中被观察者通知观察者发生了某个特定的事件。
在这种模式中,事件的触发需要遵循约定,即只能由被观察者主动触发。这符合观察者
模式的设计和语义,使得代码更易理解和维护。
通过限制事件只能在类内部触发,可以保护封装性、安全性,并使代码更一致、可
维护。
尽管用event关键字来声明事件,但实际上事件在编译后会被转换成包含两个公有
方法(add和remove)和一个私有委托字段的结构。
当定义一个事件时,编译器会自动创建一个特殊的隐藏委托字段。公有的add方法
用于添加事件处理程序,remove方法用于移除事件处理程序。
这种设计是为了提供一种更安全、更可控的方式来管理事件的订阅和取消订阅。
通过将事件的订阅和取消订阅限制在特定的add和remove方法中,可以确保事件只能在
类内部触发,并且可以对事件的订阅做额外的逻辑处理,如验证订阅条件等。
由于事件在编译后被转换成公有的add和remove方法,因此事件无法直接被外部调
用。只能通过`+=`和`-=`操作符来订阅和取消订阅事件,让编译器自动生成的add和
remove方法来处理相关逻辑。
问:普通委托与事件的区别?
答:委托(Delegate)和事件(Event)是用于实现事件驱动编程的重要概念,区别如下:
(1)定义和使用方式:
委托是一种类型,用于定义方法的签名和调用方式。它可以理解为对方法的抽象,允
许将方法作为参数传递、返回方法以及创建方法的引用。使用委托可以实现方法的回调。
事件是委托的一种特殊用法。它是一种机制,用于将事件触发和事件处理程序进行解
耦。事件需要先定义一个委托类型,然后可以使用该事件类型声明事件成员。通过事件成
员,可以添加、移除和触发事件处理程序。
(2)触发和处理方式:
委托的调用是直接的,可以通过委托的实例直接调用相应的方法。
事件采用了发布-订阅模型,由事件的发布者(发布事件的对象)触发事件,事件的订
阅者(事件处理程序)通过订阅事件来接收并处理事件。
(3)安全性和封装性:
委托允许直接调用方法,因此具有更高的灵活性。但也可能导致潜在的不安全因素,
因为任何具有委托实例的代码都可以调用委托所引用的方法。
事件通过封装委托的添加和移除过程,对事件的触发进行限制,提供了更好的安全性
和封装性。事件的发布者只能触发事件,而不能直接调用事件处理程序。
总结:委托更加通用,用于实现方法回调和动态调用方法。而事件则是基于委托的特
殊机制,用于实现发布-订阅模型,实现对象间的解耦和通信。
解耦:简言之,降低对象之间的相关性,使它们更加独立。便于独立开发、维护、扩展
下面观察一下接口情况:
internal interface IInterface
{
int Age { get; set; }
void SayHI();
string this[int index] { get; set; }
event Action MyEvent;//这是事件,视为方法
//Action MyEvent;//错误,这是字段
}
internal class MyClas : IInterface
{
public string this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public int Age { get; set; }
public event Action MyEvent;
public void SayHI()
{ Console.WriteLine("Hello"); ; }
}
上面接口中事件实际是add,remove两个方法,所以可以出现在接口中。
4、EventHandler介绍
EventHandler是一个在常用的预定义委托类型,常用于定义通用的事件处理委托。它
可以处理不带返回值且带有EventArgs参数的事件。
EventHandler委托的定义如下:
public delegate void EventHandler(object sender, EventArgs e);
sender:表示事件的发送者,通常是发布事件的对象。
e:表示事件相关的数据,通常使用EventArgs或其派生类传递额外的信息给事
件处理程序。
使用EventHandler委托例子:
(1)声明事件:
class MyClass
{
public event EventHandler MyEvent;
}
在MyClass类中声明了一个MyEvent事件,它使用了EventHandler委托。
(2)添加事件处理程序:
MyClass obj = new MyClass();
obj.MyEvent += MyEventHandler;
使用+=操作符将事件处理程序(委托实例)添加到事件上,这里
MyEventHandler是一个方法。
(3)定义事件处理程序:
static void MyEventHandler(object sender, EventArgs e)
{
// 处理事件的逻辑
}
MyEventHandler是一个符合EventHandler委托签名的方法,用于处理MyEvent事件。
(4)触发事件:
if (MyEvent != null)
{
MyEvent(this, EventArgs.Empty);
}
在类内部的适当位置,您可以使用事件名后跟圆括号的方式来触发事件。这里,
使用MyEvent(this, EventArgs.Empty);来触发MyEvent事件,并传递this作为发送
者,EventArgs.Empty表示事件不传递额外的信息。
总结:EventHandler是一个常用的预定义委托类型,用于定义处理不带返回值且
带有EventArgs参数的事件。可以通过声明事件、添加事件处理程序、定义事件处理
程序和触发事件来使用EventHandler委托。
EventHandler=Event+Handler
Event表示事件的概念,用于发布和触发通知。
Handler表示处理事件的方法或委托。可被认为是函数的指针,目标方法的引用。
5、介绍一下EventArgs类
EventArgs类是一个常用的类,用于向事件处理程序传递事件相关的信息。它是一个基
类,可以被其他自定义的事件参数类继承。
EventArgs类本身非常简单,它没有任何成员或属性。它的主要目的是为了在事件
触发时提供一个通用的基类,以便其他事件参数类可以通过继承并扩展它来传递更具
体的事件信息。
当定义一个事件时,通常会以EventHandler委托的形式声明事件处理程序的签名。
这个委托通常接受两个参数:第一个参数是触发事件的对象,第二个参数是派生自
EventArgs类的事件参数对象。
例,有一个自定义的事件叫做MyEvent,我们可以定义一个对应的事件参数类
MyEventArgs,它继承自EventArgs类,并添加一些额外的属性来携带事件相关的信息。
然后我们可以使用这个事件参数类作为EventHandler委托的第二个参数类型,来传递
事件信息给事件处理程序。
public class MyEventArgs : EventArgs// 自定义事件参数类
{
public int Data { get; set; }
}
public class MyClass// 包含事件的类
{
public event EventHandler MyEvent;//a
public void OnMyEvent(int data)
{
MyEventArgs args = new MyEventArgs();
args.Data = data;
MyEvent?.Invoke(this, args);//b 触发事件,传递事件参数
}
}
public class Program// 使用事件的类
{
private static void Main(string[] args)
{
MyClass myObj = new MyClass();
myObj.MyEvent += MyEventHandler;
myObj.OnMyEvent(42); // 触发事件
}
private static void MyEventHandler(object sender, MyEventArgs e)
{
Console.WriteLine("Event data: " + e.Data);
}
}
当MyEvent事件触发时,会创建一个MyEventArgs对象,并将数据设置为特定的值,
然后通过MyEvent事件触发并传递这个事件参数对象。事件处理程序MyEventHandler
可以通过访问MyEventArgs对象的属性来获取这个数据,并进行相应的处理。
问:上面a处并没有说明参数的个数,b处为何知道是两个参数?
答:EventHandlero为一个没有返回值的方法,接受两个参数。
EventHandler委托有重载:
(1)EventHandler委托(非泛型):
第一个参数object类型的参数作为事件的发送者,第二个EventArgs类型的参数作为事件的参数
public delegate void EventHandler(object sender, EventArgs e);
(2)EventHandler
第一个object类型,第二个泛型类型TEventArgs的参数作为事件的参数(继承自EventArgs)
public delegate void EventHandler(object sender, TEventArgs e);
因此接受两个参数,即一个object类型,一个自定义事件参数类型 MyEventArgs。
再看一下流程:
上面我们有一个包含事件的类MyClass,该类作为发布者(Publisher)负责触发事
件并发送事件信息。
发布者(Publisher)是指包含事件的对象,它负责触发事件并通知所有已注册的订
阅者(Subscribers)。在示例中,MyClass就是发布者。
订阅者(Subscriber)是指对事件感兴趣并注册了事件处理程序的对象。订阅者可
以是任何类型的对象,只要它们能够与事件处理程序的签名匹配。在示例中,我们使用
Main方法中的MyEventHandler作为订阅者。
注册(Register)是指将一个事件处理程序方法与事件进行关联,以便在事件触发
时执行该方法。在示例中,我们使用+=操作符将MyEventHandler方法注册为MyEvent事件
的处理程序。
触发(Trigger)是指在事件发生时通知所有已注册的订阅者执行相应的事件处理程
序。在示例中,OnMyEvent方法触发了MyEvent事件,并将事件参数传递给订阅者的事件
处理程序。
事件参数(Event Args)是用于传递事件相关信息的对象。在示例中,我们创建了
一个自定义的事件参数类MyEventArgs,其中包含了一个Data属性来携带事件数据。当
事件触发时,我们将一个MyEventArgs对象作为参数传递给事件处理程序。
总结:发布者负责触发事件并传递事件参数,订阅者负责注册事件处理程序并在事
件触发时执行相应的逻辑。通过事件和事件参数,发布者和订阅者之间可以进行解耦,
实现松耦合的交互。
下面再结合前面发布、订阅、注册、参数等举例:
public class MyEventArgs : EventArgs//自定义参数类
{
public string message { get; }
public MyEventArgs(string msg)
{ this.message = msg; }
}
public class Publisher//发布者(含事件及触发)
{
public event EventHandler MyEvent;
protected virtual void OnMyEvent(MyEventArgs e)//触发事件
{ MyEvent?.Invoke(this, e); }
public void RaiseEvent(string s)//触发事件的方法
{ OnMyEvent(new MyEventArgs(s)); }
}
public class Subscriber//订阅者
{
public void HandleEvent(object sender, MyEventArgs e)//事件处理
{ Console.WriteLine($"应该显示什么消息呢?{e.message}"); }
}
public class Program// 使用事件的类
{
private static void Main(string[] args)
{
Publisher p = new Publisher();//发布者实例
Subscriber s = new Subscriber();//订阅者实例
p.MyEvent += s.HandleEvent;//注册事件处理,把上面两者关联
p.RaiseEvent("来了老弟");//触发事件
Console.ReadKey();
}
}
首先我们定义了一个包含数据的事件参数类MyEventArgs,它具有一个命名为Message的属
性。然后,我们创建了一个发布者类Publisher,其中包含了一个MyEvent事件,以及一个
用于触发事件的方法RaiseEvent和一个保护方法OnMyEvent用于实际触发事件。接下来,
我们创建了一个订阅者类Subscriber,其中包含了处理事件的方法HandleEvent。最后,
在主程序中,我们创建了一个发布者对象和订阅者对象,并通过使用+=操作符将订阅者的
事件处理程序注册到发布者的事件上。然后,我们调用发布者的RaiseEvent方法来触发事
件,并传递一个消息作为参数。
当事件被触发时,订阅者的事件处理程序会被调用,并输出接收到的消息。
6、委托和事件的区别(常考)
委托和事件没有可比性,因为委托是类型,事件是对象(可以理解为对委托变量的封装)
下面说的是委托的对象(用委托方式实现的事件)和(标准的event方式实现)事件的区别。
事件的内部是用委托实现的。(举例子:三种实现事件方式的区别)
因为对于事件来讲,外部只能“注册自己+=、注销自己-=”,外界不可以注销其他的注册
者,外界不可以主动触发事件,因此如果用Delegate就没法进行上面的控制,因此诞生
了事件这种语法。add、remove。
为什么?
答:对于事件,外部只能注册自己以及注销自己,而不能直接注销其他注册者。这是为
了确保安全性和封装性。
事件的设计初衷是为了实现一种观察者模式,即通过事件发布者通知所有订阅者,
而不需要订阅者之间直接通信。因此,事件的发布者应该控制事件的触发和通知过程,
而不应该允许外部直接修改事件的注册列表。
通常通过使用event关键字来定义事件,并使用+=和-=运算符来添加或移除事件的处
理方法。这种语法保证了外部只能注册和注销自己的事件处理方法,而无法直接修改其
他订阅者的列表。
这种设计有助于提高代码的安全性和可维护性。发布者可以更好地控制事件的触发
逻辑,并避免不可预料的行为和潜在的安全问题。同时,订阅者也可以更好地理解自己
对事件的注册和注销行为,避免不必要的依赖和耦合。
注意:对于事件,无法直接获取事件委托中的调用列表(Invocation List)。
事件是用来阉割委托实例的。事件只能add、remove自己,不能赋值。
事件只能+=、-=,不能=、不能外部触发事件。
问:事件常以On开头吗?
答:On表示"触发"或"激活"一个事件的意思。
触发事件通常使用约定的命名模式,以"On"开头加上事件名称的方式。这是一个常
见的命名约定,用于表示事件的触发。例如,假设有一个名为"Click"的事件,那么触
发事件的方法可以命名为"OnButtonClick"。
internal class Button
{
public event EventHandler Click;
protected virtual void OnButtonClick(EventArgs e)
{
Click?.Invoke(this, e);
}
}
注意:这只是一个约定,并不是C#编译器所要求的强制规则。
7、案例:登录界面合并成用户自定义控件。通过事件传参来适用程序不同地方调用这个登
录界面。
(1)创建自定控件UCLogin。新建窗体程序,右击Form1->添加->用户控件。
里面添加两lable(用户名,密码),两txtbox(txtUser,txtPwd),一个Button(btnLog)
双击登录button,为这个登录button添加触发事件:
public partial class UCLogin : UserControl
{
public UCLogin()
{
InitializeComponent();
}
public event EventHandler ClickLog;//声明事件
private void btnLog_Click(object sender, EventArgs e)//a 点击事件
{
LogArgs args = new LogArgs();
args.IsLogined = false;
args.User = txtUser.Text.Trim();//自己判断为空退出
args.Pwd = txtPwd.Text.Trim();
ClickLog?.Invoke(this, args);//b 触发事件
if (args.IsLogined)
{
this.BackColor = Color.GreenYellow;
}
else
{
this.BackColor = Color.Red;
}
}
}
public class LogArgs : EventArgs//c 事件参数类(第二个)
{
public string User { get; set; }
public string Pwd { get; set; }
public bool IsLogined { get; set; }
}
(2)把上面自定义控件UClogin添加到Form1中,双击UClogin,在其加载时注册事件,
以便在点击事进行处理。
private void ucLogin1_Load(object sender, EventArgs e)
{
ucLogin1.ClickLog += UcLogin1_ClickLog;//d 注册
}
private void UcLogin1_ClickLog(object sender, LogArgs e)//e 具体处理
{
if (e.User == "admin" && e.Pwd == "8888")
{
e.IsLogined = true;
}
}
上面a是发布者,在b触发事件,d是注册,e是订阅者。
d处只能注册和注销自己,不能操作其它注册者。
1、什么是程序集?
程序集是.net中的概念。.net中的dll与exe文件都是程序集。
注意:这里的exe和dll和C编译的exe和dll不同。这里是指.net中,因为它们都是IL.
放在Reflector是可以查看的,但不能查看C编译的(因为它是机器码)
(exe与dll的区别?)
exe是包含入口点的程序集,可以独立地运行和执行.动态链接库(dll)是可重用
的代码库,并不能直接运行。它主要用于存储和组织代码、类和方法,供其他程序集
引用和调用。
程序集(Assembly),可以看做是一堆相关类打一个包,相当于java中的jar包(*)。
程序集包含:
类型元数据(描述在代码中定义的每一类型和成员,二进制形式)、
程序集元数据(程序集清单、版本号、名称等)、
IL代码(这些都被装在exe或dll中)、
资源文件。每个程序集都有自己的名称、版本等信息。这些信息可以通过
AssemblyInfo.cs文件来自己定义。
2、使用程序集的好处?
程序中只引用必须的程序集,减小程序的尺寸。
程序集可以封装一些代码,只提供必要的访问接口。
如何添加程序集的引用?
添加路径、项目引用、GAC(全局程序集缓存)
不能循环添加引用
在c#中添加其他语言编写的dll文件的引用。
(参考P/Invoke,在.net中调用非程序集的dll)extern
1、反射无处不在,我们天天在使用。
Vs的智能提示,就是通过反射获取到类的属性、方法等。
还有反编译工具也是通过反射实现
反射就是动态获取程序集的元数据(提供程序集的类型信息)的功能
反射:就是动态获取程序集中的元数据来操作类型的。
Type类实现反射的一个重要的类,通过它我们可以获取类中的所有信息包括方法、属性
等。可以动态调用类的属性、方法。
Type是对类的描述。如何获取Person类中的所有属性?
反射就是直接通过.dll来创建对象,调用成员。
反射犹如一面镜子。在你面前有一面镜子。这面镜子可以反射出它所对应的物体
的影像。类似地,C#反射也可以“反射”出程序集,类型和成员的信息。
在这个比喻中,程序集可以被看作是一个房间,包含许多不同的对象和成员。类
型可以是房间中不同的物体,而成员则是物体的属性或方法。
当你站在这面镜子前时,你可以透过镜子看到房间内的物体。同样地,C#反射可
以让你在运行时通过编码查看程序集、类型和成员的详细信息,而不需要事先知道它
们的结构。
使用C#反射,你可以动态地加载程序集,检查类型的属性和方法,获取和设置成
员的值,调用方法等等。就像你可以通过镜子观察房间内的物体一样,C#反射使得在
运行时检查和操作代码成为可能。
反射是一种强大的工具,尽管它的使用可能会稍微复杂一些,但在某些情况下它
是十分有用的。比如,当你需要在运行时动态地加载和使用程序集时,或者需要在未
知类型上调用方法时,C#反射就能够派上用场。
案例:先通过一个普通类介绍Type.(Person)
通过类型元数据来获取对象的一些相关信息,并且还可以实例化对象调用方法等,
这个就叫做“反射”。反射让创建对象的方式发生了改变。编译器的智能提示就是反射
的一个应用。
internal class Program
{
private static void Main(string[] args)
{
Person p = new Person();
Type t1 = p.GetType();//实例
Type t2 = typeof(Person);//类名
FieldInfo[] f = t1.GetFields();
foreach (FieldInfo item in f)
{ Console.WriteLine(item.Name); }
MethodInfo[] mi = t1.GetMethods();
foreach (MethodInfo mi2 in mi)
{ Console.WriteLine(mi2.Name); }
MemberInfo[] mbi = t1.GetMembers();
foreach (MemberInfo mi3 in mbi)
{ Console.WriteLine(mi3.Name); }
Console.ReadKey();
}
}
internal class Person
{
private string _name;
public int Age;
public string ID { get; set; }
public string JiBie { get; set; }
public void SayHi()
{ Console.WriteLine("hello"); }
}
2、几个相关类
(1)FieldInfo类
是System.Reflection命名空间中的一个类,用于表示和操作类中的字段(成员变量)
属性:
IsPublic:获取一个值,该值指示字段是否为公共字段。
IsPrivate:获取一个值,该值指示字段是否为私有字段。
IsStatic:获取一个值,该值指示字段是否为静态字段。
IsInitOnly:获取一个值,该值指示字段是否为只读字段。
FieldType:获取字段的类型。
Name:获取字段的名称。
用法:
(a)获取FieldInfo对象:
使用Type类的GetField方法,根据字段名称获取单个字段的FieldInfo对象。
使用Type类的GetFields方法,获取所有公共的非静态字段的FieldInfo对象数组。
(b)获取和设置字段的值:
使用FieldInfo的GetValue方法,传入实例对象,获取字段的值。
使用FieldInfo的SetValue方法,传入实例对象和新值,设置字段的值。
internal class Program
{
private static void Main(string[] args)
{
Type t = typeof(MyClass);
FieldInfo field = t.GetField("myField");//单个字段获取
Console.WriteLine($"{field.Name},{field.FieldType.Name}");//myField,string
FieldInfo[] fis = t.GetFields();
foreach (FieldInfo f in fis)
{
Console.WriteLine(f.Name);//myField、myStaticField
Console.WriteLine(f.FieldType.Name);//String、Int32
}
MyClass obj = new MyClass();
field.SetValue(obj, "Hello");
Console.WriteLine(field.GetValue(obj));
Console.ReadKey();
}
}
public class MyClass
{
public string myField;
private int myPrivateField;
public static int myStaticField;
}
(2)MethodInfo类
是C#中System.Reflection命名空间中的一个类,用于表示和操作类中的方法(成员函数)。
属性:
IsPublic:获取一个值,该值指示方法是否为公共方法。
IsPrivate:获取一个值,该值指示方法是否为私有方法。
IsStatic:获取一个值,该值指示方法是否为静态方法。
ReturnType:获取方法的返回类型。
Name:获取方法的名称。
GetParameters():获取方法的参数信息。
方法:
Invoke():调用方法。
GetCustomAttributes():获取方法的自定义特性信息。
用法:
(a)获取MethodInfo对象:
使用Type类的GetMethod方法,根据方法名称获取单个方法的MethodInfo对象。
使用Type类的GetMethods方法,获取所有公共的非静态方法的MethodInfo对象数组。
(b)调用方法:
使用MethodInfo的Invoke方法,传入实例对象和方法的参数,调用方法并获取返回值。
(c)获取方法的参数信息:
使用MethodInfo的GetParameters方法,获取方法的参数的ParameterInfo对象数组。
通过ParameterInfo可以获取参数的名称、类型等信息。
internal class Program
{
private static void Main(string[] args)
{
Type t = typeof(MyClass);
MethodInfo m = t.GetMethod("MyMethod");
Console.WriteLine($"{m.Name},{m.ReturnType.Name}");//MyMethod,Int32
MethodInfo[] ms = t.GetMethods();
foreach (MethodInfo m1 in ms)
{
Console.WriteLine($"{m1.Name},{m1.ReturnType.Name} ");
}
MyClass obj = new MyClass();
object[] parameters = new object[] { "Hello", 5 };
object result = m.Invoke(obj, parameters);
Console.WriteLine(result);
Console.ReadKey();
}
}
public class MyClass
{
public int MyMethod(string str, int num)
{
Console.WriteLine(str);
return num * 2;
}
private void MyPrivateMethod()
{ Console.WriteLine("This is a private method."); }
}
(3)Parameters类
是C#中System.Reflection命名空间中的一个类,用于表示和操作方法或构造函数的参数。
属性:
Name:获取参数的名称。
ParameterType:获取参数的类型。
IsOptional:获取一个值,该值指示参数是否为可选参数。
DefaultValue:获取参数的默认值。
注意:对于没有指定默认值的参数,HasDefaultValue属性将返回false,
而DefaultValue属性将返回null。只有当参数指定了默认值时,HasDefaultValue
属性才会返回true,并且DefaultValue属性将返回其指定的默认值。
方法:
GetCustomAttributes():获取参数的自定义特性信息。 用法:
(a)获取ParameterInfo对象:
使用MethodInfo类的GetParameters方法,获取方法的参数的ParameterInfo对象数组。
(b)获取参数信息:
使用ParameterInfo的Name属性,获取参数的名称。
使用ParameterInfo的ParameterType属性,获取参数的类型。
使用ParameterInfo的IsOptional属性,判断参数是否为可选参数。
使用ParameterInfo的DefaultValue属性,获取参数的默认值。
(c)获取参数的自定义特性信息:
使用ParameterInfo的GetCustomAttributes方法,获取参数的自定义特性。
internal class Program
{
private static void Main(string[] args)
{
Type t = typeof(MyClass);
MethodInfo m = t.GetMethod("MyMethod");
ParameterInfo[] ps = m.GetParameters();
foreach (ParameterInfo pi in ps)
{
Console.WriteLine(pi.Name);
Console.WriteLine(pi.ParameterType.Name);
Console.WriteLine(pi.IsOptional);
Console.WriteLine(pi.DefaultValue);
object[] attributes = pi.GetCustomAttributes(true);
foreach (object item in attributes)
{
Console.WriteLine(item.GetType().Name);
}
}
Console.ReadKey();
}
}
public class MyClass
{
public void MyMethod(string str, int num, bool flag = true)
{
Console.WriteLine(str);
Console.WriteLine(num);
Console.WriteLine(flag);
}
}
通过ParameterInfo类,我们可以获取方法的参数信息,如参数名称、参数类型、是否
可选以及默认值等。同时,我们还可以使用ParameterInfo类获取参数的自定义特性,从而
进一步扩展方法的功能或特定参数的行为。
(4)MemberInfo类
是C#中反射机制的一部分,它是一个抽象基类,用于表示一个类的成员(如字段、方法、
属性等)的信息。MemberInfo类提供了一些属性和方法来获取和操作成员的相关信息。
属性:
MemberType:获取MemberInfo的成员类型。它是一个枚举值,可以是Constructor、
Event、Field、Method、Property等。
DeclaringType:获取定义该成员的类型。
Name:获取成员的名称。
CustomAttributes:获取成员的自定义特性。 方法:
GetCustomAttributes(bool inherit):获取应用于成员的所有自定义特性。参数
inherit指定是否搜索继承链以查找特性。
IsDefined(Type attributeType, bool inherit)`:检查成员是否应用了指定类型的
特性。参数inherit指定是否搜索继承链。
GetCustomAttributesData():获取表示应用于成员的自定义特性的
CustomAttributeData对象的列表。
ToString():返回成员的字符串表示形式。
internal class Program
{
private static void Main(string[] args)
{
Type t = typeof(MyClass);
MemberInfo[] mis = t.GetMembers();
foreach (MemberInfo mi in mis)
{
Console.WriteLine($"{mi.Name}:{mi.MemberType}");
object[] attributes = mi.GetCustomAttributes(true);
foreach (object attr in attributes)
{
Console.WriteLine(attr.ToString());
}
}
Console.ReadKey();
}
}
internal class MyClass
{
public int MyField;
[Obsolete("This method is obsolete.")]
public void MyMethod()
{ }
public string MyProperty { get; set; }
}
Equals:Method
__DynamicallyInvokableAttribute
GetHashCode:Method
__DynamicallyInvokableAttribute
GetType:Method
System.Security.SecuritySafeCriticalAttribute
__DynamicallyInvokableAttribute
ToString:Method
__DynamicallyInvokableAttribute
.ctor:Constructor
MyProperty:Property
MyField:Field
注意:由于MemberInfo是一个抽象基类,它的具体实例一般是由其派生类(如FieldInfo、
MethodInfo等)来表示具体的成员类型。因此,可以使用MemberInfo类的派生类的特
定方法和属性来获取更详细的成员信息。
3、看一下Type类实际应用。
(1)假定有一个类库的项目(右击解决方案->添加->新项->类库,TestDll)
public class Person
{
public Person()
{ }
public Person(string name, int age, string email)
{ this.Name = name; this.Age = age; this.Email = email; }
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
public void SayHi()
{ Console.WriteLine("hello"); }
public void SayHello()
{ Console.WriteLine("hi"); }
public void SayHello(string msg)
{ Console.WriteLine(msg); }
public void SayHello(string msg, int age)
{ Console.WriteLine(msg + age.ToString()); }
public int Add(int n1, int n2)
{ return n1 + n2; }
}
public interface IFlyable
{ void Fly(); }
public class MyClass1 : IFlyable
{
public void Fly()
{ Console.WriteLine("飞呀"); }
}
public class Student : Person
{ }
public delegate void MyDelegate();
public struct MyStruct
{ }
internal struct MyStruct1
{ }
public enum MyEnum
{ }
(2)再新建一个项目,来应用各成员:
private static void Main(string[] args)
{
Assembly asm = Assembly.LoadFile(@"D:\OneDrive\CSharp\Test\TestDll\bin\Debug\TestDll.dll");//动态加载程序集
Type[] t1 = asm.GetTypes();//1,获取该程序集中的所有类型
foreach (Type t9 in t1)//a
{ Console.WriteLine(t9.FullName); }
Type[] t2 = asm.GetExportedTypes();//2.获取所有的public的类型
foreach (Type t9 in t2)//b
{ Console.WriteLine(t9.FullName); }
//3.获取指定类型
Type tp = asm.GetType("TestDll.Person");
object p = Activator.CreateInstance(tp);//创建Perosn对象,不能直接Person p=new...
MethodInfo mi = tp.GetMethod("SayHi");
mi.Invoke(p, null);
//若有重载时,通过第二参指定Type[]是哪一个方法
MethodInfo mi1 = tp.GetMethod("SayHello", new Type[] { });//取无参SayHello
mi1.Invoke(p, null);
MethodInfo mi2 = tp.GetMethod("SayHello", new Type[] { typeof(string) });//取一参string
object[] ps = new object[] { "大家好" };//参数列表,object数组形式,适应各参数类型
mi2.Invoke(p, ps);
MethodInfo mi3 = tp.GetMethod("SayHello", new Type[] { typeof(string), typeof(int) });//取两参
mi3.Invoke(p, new object[] { "小旺旺", 16 });//两个参数,实参
//上面方法若有返回值,直接用变量接收上面的invoke返回值即可
MethodInfo[] mis = tp.GetMethods().Where(m => m.Name == "SayHello").ToArray();//取同名重载个数
foreach (MethodInfo m9 in mis)
{ Console.WriteLine($"{m9.Name}:{m9.GetParameters().Length}"); }
MethodInfo[] miss = tp.GetMethods();
foreach (MethodInfo m9 in miss)
{ Console.WriteLine($"{m9.Name},{m9.MemberType}"); }
//==================下面由Type来创建对象=========
//MethodInfo mi4 = tp.GetMethod("SayHello");//重载时,这个会出错
//如何知道是否重载
MethodInfo[] misA = tp.GetMethods().Where(m => m.Name == "SayHello").ToArray();
Console.WriteLine(misA.Length);//3 大于1表示有重载
ParameterInfo pi = misA[1].GetParameters()[0];
Console.WriteLine(pi.ParameterType.Name);//取得第二重载的第一个参数类型
//严格说下面是有缺陷的,有可能类没有无参构造
object obj = Activator.CreateInstance(tp);//所以它的第二个参数type[]就带构造函数参数的。
object obj1 = Activator.CreateInstance(tp, new object[] { "刀郎", 45, "[email protected]" });
//用专门的构造函数来取得
//a,先取得构造方法类型
ConstructorInfo ci = tp.GetConstructor(new Type[] { typeof(string), typeof(int), typeof(string) });
//b,再由构造方法来创建实例
object obj2 = ci.Invoke(new object[] { "张振", 20, "[email protected]" });
//c,此时调用属性,不能直接用属性,因为属性也不知道,又得反射取得属性信息类型
PropertyInfo prop = tp.GetProperty("Name");
string name = prop.GetValue(obj2, new object[] { }).ToString();
Console.WriteLine(name);
Console.ReadKey();
}
4、学了上面仍然是一头雾水,那么请看看下面:
(1)问:反射之前都不知道类的情况,为什么前面的例子需要参考TestDll来写?
答:的确是无法预知的,通常的做法是逐步列举该类的成员、参数等信息,然后进行相
应的调用。因为初学,所以省去前面无关的大量的列举代码,方便快速入门。
通用步骤是:
首先,我们需要获取要反射的类型信息,可以通过程序集中的某个类型的完全限定
名(包括命名空间)来获取该类型的Type对象,例如使用Type.GetType()方法。如果该
类型位于当前程序集中,也可以使用该类型的typeof关键字来获取Type对象。
然后,我们可以通过Type对象获取该类型的所有成员信息,例如使用GetMethods()
方法获取所有方法、GetProperties()方法获取所有属性等。再根据需要,我们可以遍
历这些成员信息,逐个进行操作。
对于方法调用,我们需要获取方法的参数类型,一般使用GetParameters()方法来
获取方法的参数信息,再根据参数信息创建相应的参数值。最后,我们使用MethodInfo
对象的Invoke()方法来调用该方法。
注意,在进行反射操作时,我们需要处理成员的访问修饰符以及其他边界情况。不
可访问的私有成员、只有部分公开的嵌套类型等情况可能会导致反射操作失败或产生异
常。
总结,当我们对一个类的结构无法预知时,通常需要逐步列举成员、参数等信息,
并按需进行调用。这种方式确实需要额外的工作量和复杂性,但反射提供了一种在运行
时动态访问和操作类成员的能力。
(2)问:用type后,并不能用.来引出相对就的成员,必须老老实实地,分别独立创建字
段,方法,属性,再用这些来实现。感觉有点繁琐?
答:是的。
在 C# 中,通过反射获取类型信息后,我们不能直接使用.来访问其中的成员(字
段、方法、属性等),而是需要使用反射提供的特定方法来访问相关的成员。
具体来说,在反射中,要获取类型的字段(FieldInfo)、方法(MethodInfo)、
属性(PropertyInfo)等成员信息,我们需要使用 GetFields()、GetMethods()、
GetProperties() 等方法获取对应的数组或集合。然后,我们需要遍历这些数组或集
合,逐个访问其中的成员。
这种方式的确有些繁琐,但它使得我们能够以一种通用、灵活的方式访问并操作类
型成员,特别是当我们在运行时需要动态地查找、调用或修改类型成员时,反射提供了
非常强大的功能。
如果你觉得直接使用反射的方式比较繁琐,也可以考虑使用第三方库或框架来简化
相关操作。这些库或框架通常提供了更方便的 API,可以减少我们编写繁琐的反射代码
的工作量。不过,需要注意选择可信赖、稳定的第三方库,并根据具体需求进行评估和
选择。
(3)问:反射只能对.net程序吗?
答:是的。
反射是一种用于在运行时动态地检查、访问和操作程序的能力。在 .NET 平台中,
C# 提供了内置的反射功能,可以通过 System.Reflection 命名空间中的类来实现对
程序集、类型和成员的反射操作。
然而,在传统的 C 和 C++ 编程语言中,并没有内置的反射机制。这意味着在这
些语言中,我们无法直接使用反射来动态地获取和操作类型的成员(可以考虑使用其他
技术和方法)。
注意,反射操作可能会带来性能开销,并且需要谨慎处理权限和边界情况。此外,
只有公共成员或使用适当的权限设置的非公共成员才能被反射访问和操作。
5、Type类
Type类是一个表示类型的类,用来获取有关类型的信息。
Name属性:获取类型的名称。
Type t = typeof(int);
Console.WriteLine(t.Name); // 输出 "Int32"
FullName属性:获取类型的完全限定名称,包括命名空间。
Type t = typeof(System.Collections.ArrayList);
Console.WriteLine(t.FullName); // 输出 "System.Collections.ArrayList"
IsArray属性:判断类型是否为数组类型。
Type t = typeof(int[]);
Console.WriteLine(t.IsArray); // 输出 "True"
IsClass属性:判断类型是否为类类型(引用类型)。
Type t = typeof(string);
Console.WriteLine(t.IsClass); // 输出 "True"
IsEnum属性:判断类型是否为枚举类型。
Type t = typeof(System.DayOfWeek);
Console.WriteLine(t.IsEnum); // 输出 "True"
GetMethods方法:获取类型的所有公共方法。
Type t = typeof(string);
MethodInfo[] methods = t.GetMethods();
foreach (MethodInfo method in methods)
{
Console.WriteLine(method.Name);
}
GetProperties方法:获取类型的所有公共属性。
Type t = typeof(Person);
PropertyInfo[] properties = t.GetProperties();
foreach (PropertyInfo property in properties)
{
Console.WriteLine(property.Name);
}
GetConstructors方法:获取类型的所有公共构造函数。
Type t = typeof(Person);
ConstructorInfo[] constructors = t.GetConstructors();
foreach (ConstructorInfo constructor in constructors)
{
Console.WriteLine(constructor.Name);
}
GetInterfaces方法:获取类型实现的所有接口。
Type t = typeof(Person);
Type[] interfaces = t.GetInterfaces();
foreach (Type iface in interfaces)
{
Console.WriteLine(iface.Name);
}
GetField方法:获取类型的公共字段。
Type t = typeof(Person);
FieldInfo field = t.GetField("Name");
Console.WriteLine(field.Name);
6、PropertyInfo类
PropertyInfo类是C#中的一个反射类,它用于获取和操作类的属性信息。它提供了许多
属性和方法,可以帮助我们在运行时动态地获取和设置属性的值。
(1)PropertyType:获取属性的类型。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
Type propertyType = property.PropertyType;
(2)Name:获取属性的名称。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
string propertyName = property.Name;
(3)GetValue:获取属性的值。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
object propertyValue = property.GetValue(instance);
(4)SetValue:设置属性的值。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
property.SetValue(instance, value);
(5)GetCustomAttributes:获取属性的自定义特性。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
var attributes = property.GetCustomAttributes(typeof(MyAttribute), true);
(6)CanRead:判断属性是否可读。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
bool canRead = property.CanRead;
(7)CanWrite:判断属性是否可写。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
bool canWrite = property.CanWrite;
(8)IsStatic:判断属性是否为静态属性。
PropertyInfo property = typeof(MyClass).GetProperty("MyProperty");
bool isStatic = property.GetGetMethod().IsStatic;
7、Constructor类
ConstructorInfo类是C#中的一个反射类,用于获取和操作类的构造函数信息。
(1)Invoke:调用构造函数创建对象实例。
ConstructorInfo constructor = typeof(MyClass).GetConstructor(new Type[] { typeof(int), typeof(string) });
var instance = constructor.Invoke(new object[] { 10, "hello" });
(2)isStatic:判断构造函数是否为静态构造函数。
ConstructorInfo constructor = typeof(MyClass).GetConstructor(new Type[] {});
bool isStatic = constructor.IsStatic;
(3)isPublic:判断构造函数是否为公共构造函数。
ConstructorInfo constructor = typeof(MyClass).GetConstructor(new Type[] { typeof(int) });
bool isPublic = constructor.IsPublic;
(4)GetParameters:获取构造函数的参数信息。
ConstructorInfo constructor = typeof(MyClass).GetConstructor(new Type[] { typeof(int), typeof(string) });
ParameterInfo[] parameters = constructor.GetParameters();
(5)GetCustomAttributes:获取构造函数的自定义特性。
ConstructorInfo constructor = typeof(MyClass).GetConstructor(new Type[] { typeof(int) });
var attributes = constructor.GetCustomAttributes(typeof(MyAttribute), true);
(6)IsAbstract / IsVirtual:判断构造函数是否为抽象构造函数或虚构造函数。
ConstructorInfo constructor = typeof(MyClass).GetConstructor(new Type[] { });
bool isAbstract = constructor.IsAbstract;
bool isVirtual = constructor.IsVirtual;
8、Activator类
Activator类是C#中的一个工具类,提供了一些方便的方法,用于在运行时动态地创建对象和调用构造函数。下面是Activator类的常用属性和方法,以及它们的用法举例:
(1)CreateInstance:创建对象实例。
var instance = Activator.CreateInstance(typeof(MyClass));
(2)CreateInstance
var instance = Activator.CreateInstance();
(3)CreateInstance(Type, params object[]):根据参数列表来创建对象实例。
var instance = Activator.CreateInstance(typeof(MyClass), 10, "hello");
(4)IsGenericType:判断类型是否为泛型类型。
bool isGenericType = typeof(MyClass).IsGenericType;
(5)GetGenericArguments:获取泛型类型的类型参数。
Type[] genericArguments = typeof(MyClass).GetGenericArguments();
(6)CreateInstanceFrom:从指定的程序集文件中创建对象实例。
var instance = Activator.CreateInstanceFrom("MyAssembly.dll", "MyNamespace.MyClass");
(7)GetObjectCreationDelegate:获取用于创建对象实例的委托。
var creationDelegate = Activator.GetObjectCreationDelegate(typeof(MyClass));
var instance = creationDelegate();
问:Activator类和GetConstructor方法创建实例的区别?
答:两者都可以用于创建对象实例,区别如下:
(1)Activator类是一个通用的工具类,提供了多种创建对象实例的静态方法。它可
以根据类型信息直接创建对象实例,而不需要显式获取构造函数。
Activator.CreateInstance方法可以根据传入的类型(可以是泛型类型)和参数列表
动态地创建对象实例。这使得在编写代码时更加简便,不需要显式调用GetConstructor
方法去获取构造函数对象。
var instance = Activator.CreateInstance(typeof(MyClass), 10, "hello");
(2)GetConstructor方法是反射中的一种方式,用于获取指定类型的构造函数对象。
它需要显式指定类型和构造函数的参数类型,可以在获取构造函数之后再使用
ConstructorInfo类中的Invoke方法来创建对象实例。GetConstructor方法通常用于在
更高级别的反射场景中,需要更精细的控制和灵活性。
ConstructorInfo constructor = typeof(MyClass).GetConstructor(new Type[] { typeof(int), typeof(string) });
var instance = constructor.Invoke(new object[] { 10, "hello" });
总结:使用Activator类来创建对象实例是更简洁和方便的方式,适用于大多数普
通的场景。而通过GetConstructor方法获取构造函数,然后使用Invoke方法创建对象
实例适用于对反射操作需要更高级别控制的场景。选择使用哪种方式取决于具体的需
求和代码编写的风格。