C#7.2——编写安全高效的C#代码 c# 中模拟一个模式匹配及匹配值抽取 走进 LINQ 的世界 移除Excel工作表密码保护小工具含C#源代码 腾讯QQ会员中心g_tk32算法【C#版】
C#7.2——编写安全高效的C#代码
2018-11-07 18:59 by 沉睡的木木夕, 123 阅读, 0 评论, 收藏, 编辑
原文地址:https://docs.microsoft.com/zh-cn/dotnet/csharp/write-safe-efficient-code?view=netcore-2.1
值类型的优势能避免堆分配。而劣势就是往往伴随的数据的拷贝。这就导致了在大量的值类型数据很难的最大化优化这些算法操作(因为伴随着大量数据的拷贝)。而在C#7.2 中就提供了一种机制,它通过对值类型的引用来使代码更加安全高效。使用这个特性能够最大化的减小内存分配和数据复制操作。
这个新特性主要是以下几个方面:
- 声明一个
readonly struct
来表示这个类型是不变的,能让编译器当它做参数输入时,会保存它的拷贝。 - 使用
ref readonly
。当返回一个值类型,且大于 IntPtr.Size 时以及存储的生命周期要大于这方法返回的值的时候。 - 当用
readonly struct
修饰的变量/类大小大于 IntPtr.Size ,那么就应该作为参数输入来传递它来提高性能。 - 除非用
readonly
修饰符来声明,永远不要传递一个struct
作为一个输入参数(in parameter
),因为它可能会产生副作用,从而导致它的行为变得模糊。 - 使用
ref struct
或者readonly ref struct
,例如 Span 或 ReadOnlySpan 以字节流的形式来处理内存。
这些技术你要面对权衡这值类型和引用类型这两个方面带来的影响。引用类型的变量会分配内存到堆内存上。值类型变量只包含值。他们两个对于管理资源内存来说都是重要的。值类型当传递到一个方法或是从方法中返回时都会拷贝数据。这个行为还包括拷贝值类型成员时,该值的值( This behavior includes copying the value of this
when calling members of a value type. )。这个开销视这个值类型对象数据的大小而定。引用类型是分配在堆内存的,每一个新的对象都会重新分配内存到堆上。这两个(值类型和引用)操作都会花费时间。
readonly struct
来申明一个不变的值类型结构
用 readonly 修饰符声明一个结构体,编译器会知道你的目的就是建立一个不变的结构体类型。编译器就会根据两个规则来执行这个设计决定:
- 所有的字段必须是只读的 readonly。
- 所有的属性必须是只读的 readonly,包括自动实现属性。
以上两条足已确保没有readonly struct 修饰符的成员来修改结构的状态—— struct 是不变的
readonly public struct ReadonlyPoint3D {
public ReadonlyPoint3D (double x, double y, double z) {
this.X = x;
this.Y = y;
this.Z = z;
}
public double X { get; }
public double Y { get; }
public double Z { get; }
}
尽可能面对大对象结构体使用 ref readonly struct
语句
当这个值不是这个返回方法的本地值时,可以通过引用返回值。通过引用返回的意思是说只拷贝了它的引用,而不是整个结构。下面的例子中 Origin
属性不能使用 ref
返回,因为这个值是正在返回的本地变量:
public ReadonlyPoint3D Origin => new ReadonlyPoint3D(0,0,0);
然而,下面这个例子的属性就能按引用返回,因为返回的值一个静态成员:
private static ReadonlyPoint3D origin = new ReadonlyPoint3D(0,0,0);
//注意:这里返回是内部存储的易变的引用
public ref ReadonlyPoint3D Origin => ref origin;
你如果不想调用者修改原始值,你可以通过 readonly ref
来修饰返回值:
public ref readonly ReadonlyPoint3D Origin3 => ref origin;
返回 ref readonly
能够让你保存大对象结构的引用以及能够保护你内部不变的成员数据。
作为调用方,调用者能够选择 Origin
属性是作为一个值还是 按引用只读的值(ref readonly
):
var originValue = Point3D.Origin;
ref readonly var originReference = ref Point3D.Origin;
在上面这段代码的第一行,把 Point3D 的原始属性的常数值 Origin
拷贝并复制数据给originValue。第二段代码只分配了引用。要注意,readonly
修饰符必须是声明这个变量的一部分。因为这个引用是不允许被修改的。不然,就会引起编译器编译错误。
readonly
修饰符在申明的 originReference 是必须的。
编译器要求调用者不能修改引用。企图直接修改该值会引发编译器的错误。然而,编译器却无法知道成员方法修改了结构的状态。为了确定对象没有被修改,编译器会创建一个副本并用它来调用成员信息的引用。任何修改都是对防御副本(defensive copy)的修改。
对大于 System.IntPtr.Size
的参数应用 in
修饰符到 readonly struct
in
关键字补充了已经存在的 ref
和 out
关键字来按引用传递参数。in
关键字也是按引用传递参数,但是调用这个参数的方法不能修改这个值。
值类型作为方法签名参数传到调用的方法中,且没有用下面的修饰符时,是会发生拷贝操作的。每一个修饰符指定这个变量是按引用传递的,避免了拷贝。以及每个修饰符都表达不同的意图:
out
:这个方法设置参数的值来作为参数。ref
:这个方法也可以设置参数的值来作为参数。in
:这个方法作为参数无法修改这个参数的值。
增加 in
修饰符按引用传递参数以及申明通过按引用传值来避免数据的拷贝的意图。说明你不打算修改这个作为参数的对象。
对于只读的那些大小超过 IntPtr.Size
的值类型来说,这个经验经常能提高性能。例如有这些值类型(sbyte,byte,short,ushort,int,uint,long,ulong,char,float,double,decimal 以及 bool 和 enum),任何潜在的性能收益都是很小的。实际上,如果对于小于 IntPtr.Size
的类型使用按引用个传递,性能可能会下降。
下面这段 demo 展示了计算两个点的3D空间的距离
public static double CalculateDistance ( in Point3D point1, in Point3D point2) {
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
这个方法有两个参数结构体,每个都有三个 double
字段。1个 double 8 个字节,所以每个参数含有 24 字节。通过指定 in
修饰符,你传递了 4 个字节或 8 个字节的参数引用,4 还是 8字节取决平台结构(32位 一个引用 2 字节,64位一个引用 4字节)。这看似大小差异很小,但是当你的应用程序在高并发,高循环的情况下调用这个函数,那么性能上的差距就很明显了。
in
修饰符也很好的补充了 out
和 ref
其他方面。你不能创建仅修饰符(in,out,ref)不同的方法重载。这个新的特性拓展了已经存在 out
和 ref
参数原来相同的行为。像 ref
和 out
修饰符,值类型由于应用了 in
修饰符而无法装箱。
in
修饰符能应用在任何成员信息上:方法,委托,lambda表达式,本地函数,索引,操作符。
in
修饰符还有在其他方面的特性,在参数上用 in
修饰的参数值你能使用字面量的值或者常数。不像 ref
和 out
参数,你不必在调用方用 in
。下面这段代码展示了两个调用 CalculateDistance
的方法。第一个变量使用两个按引用传递的局部变量。第二个包括了作为这个方法调用的一部分创建的临时变量。
var distance = CalculateDistance (point1,point2);
var fromOrigin = CalculateDistance(point1,new Point3D());
这里有一些方法,编译器会强制执行 read-only 签名的 in 参数。第一个,被调用的方法不能直接分配一个 in 参数。它不能分配到任何 in 字段,当这个值是值类型的时候。另外,你也不能通过 ref 和 out 修饰符来传递一个 in 参数到任何方法上。这些规则都应用在 in 修饰符的参数,前提是提供一个值类型的字段以及这个参数也是值类型的。事实上,这些规则适用于多个成员访问,前提是所有级别的成员访问的类型都是结构体。编译器强制执行在参数中传递的 struct 类型,当它们的 struct 成员用作其他方法的参数时,它们是只读变量。
使用 in 参数能避免潜在拷贝方面的性能开销。它不会改变任何方法调用的语义。因此,你无需在调用方(call site)指定 in 修饰符。在调用站省略 in 修饰符会让编译器进行参数拷贝操作,有以下几种原因:
- 存在隐式转换,但不存在从参数类型到参数类型的标识转换。
- 参数是一个表达式,但是没有已知的存储变量。
- 存在一个不同于已经存在或者是不存在 in 的重载。这种情况下,通过值重载会更好匹配。
这些规则当你更新那些已有的并且已经用 read-only 引用参数的代码非常有用。在调用方法里面,你可以通过值参数(value paramters)调用任意成员方法。在那些实例中,会拷贝 in 参数。因为编译器会对 in 参数创建一个临时的变量,你可以用 in 指定默认参数的值。下面这段代码指定了origins(point 0,0)作为默认值作为第二个参数:
private static double CalculateDistance2 ( in Point3D point1, in Point3D point2 = default) {
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
编译器会通过引用传递只读参数,指定 in 修饰符在调用方法的参数上,就像下面展示的代码:
private static void DemoCalculateDistanceForExplicit (Point3D point1, Point3D point2) {
var distance = CalculateDistance ( in point1, in point2);
distance = CalculateDistance ( in point1, new Point3D ());
distance = CalculateDistance (point1, in Point3D.origin);
}
这种行为能够更容易的接受 in 参数,随着时间的推移,大型代码库中性能会获得提高。首先就要添加 in 到方法签名上。然后你可以在调用端添加 in 修饰符以及新建一个 readonly struct
类型来使编译器避免在更多未知创建防御拷贝的副本。
in 参数被设计也能使用在引用类型或数字值。然而,在这种情况的性能收益是很小的。
不要使用易变的结构体作为 in 参数
下面描述的技术主要解释了怎样通过返回引用以及传递的值引用避免数据拷贝。当参数类型是已经申明的 readonly struct
类型时,这些技术都能很好的工作。否则,编译器在很多非只读参数的场景下必须新建一个防御拷贝(defensive copies)副本。考虑下面这段代码,他计算 3D 点到原地=点的距离:
private static double CalculateDistance ( in Point3D point1, in Point3D point2 = default) {
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
Point3D
是非只读结构类型(readonly-ness struct)。在这个方法体中,有 6 个不同的属性访问调用。第一次检查时,你可能觉得这些访问都是安全的。在这之后,一个 get 读取器不能修改这个对象的状态。但是这里没有语义规则让编译器这样做。它只是一个通用的约束。任何类型都能实现 get 读取器来修改这个内部状态。没有这些语言保证,在调用任何成员之前,编译器必须新建这个参数的拷贝副本来作为临时变量。这个临时变量存储在栈上,这个参数的值的副本在这个临时变量中存储,并且每个成员访问的值都会拷贝到栈上,作为参数。在很多情况下,当参数类型不是 readonly struct
时,这些拷贝都会对性能有害,以至于通过值传递要比通过只读引用(readonly reference)传递快。
相反,如果距离计算方法使用不变结构,ReadonlyPoint3D
,就不需要临时变量:
private static double CalculateDistance3(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2 = default)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
当你用 readonly struct
修饰的成员时,编译器会自动生成更多高效代码:this
引用,而不是接受者的副本拷贝,in 参数总是按引用传递到成员方法中。当你使用 readonly struct
作为 in 参数时,这种优化会节省内存。
你可以查看程序的demo,在实例代码仓库 samples repository 中,它展示了使用 Benchmark.net 比较性能的差异。它比较了传递易变结构的值和引用,易变结构的按值传递和按引用传递。使用不变结构体的按引用传递是最快的。
使用 ref struct
类型在单个堆栈帧上处理块和内存
一个语言相关的特性是申明值类型的能力,该值类型必须约束在单个堆栈对上。这个限制能让编译器做一些优化。主要推动这个特性体检在 Span<T>
以及相关的结构。你从使用这些新添加的以及更新的.NET API,如 Span<T>
类型来完成性能的提升。
你可能有相同的要求,在内存中使用 stackalloc
或者当使用来自于内存的交互操作API。你就为这些需求能定义你自己的 ref struct 类型。
readonly ref struct
类型
声明一个 readonly ref
结构体,它联合了 ref struct
和 readonly struct
两者的收益。通过只读的元素内存被限制在单个的栈中,并且只读元素内存无法被修改。
总结
使用值类型能最小化的内存分配:
- 在局部变量和方法参数中值类型存储在栈上分配
- 对象的值类型成员做为这个对象的一部分分配在栈上,并不是一个单独的分配操作。
- 存储返回的值类型是在栈上分配
不同于引用类型在相同场景下:
- 存储局部变量和方法参数的引用类型分配在堆上,。引用存在栈。
- 存储对象的成员变量是引用类型,它作为这个对象的一部分在堆上分配内存。而不是单独的分配这个引用。
- 存储返回的值是引用类型,堆分配内存。存储引用的值存储在栈上。
最小化的内存分配要权衡。当结构体内存大小超过引用大小时,就要拷贝更多的内存。一个引用类型指定 64 字节或者是 32 字节,它取决于平台架构。
这些权衡/折中通常对性能影响很小。然而大对象结构体或大对象集合,对性能影响是递增的。特别在循环和经常调用的地方影响特别明显。
这些C#语言的增强是为了关键算法的性能而设计的,内存分配问题成为了主要的优化点。你会发现你无需经常使用这些特性在你写的代码中。然而,这些增强在 .NET 中接受。越来越多的 API 会运用到这些特性,你将看到你的应用程序性能的提升。
c# 中模拟一个模式匹配及匹配值抽取
摘一段模式的说明, F#的: msdn是这么描述它的:“模式”是用于转换输入数据的规则。模式将在整个 F# 语言中使用,采用多种方式将数据与一个或多个逻辑结构进行比较、将数据分解为各个构成部分,或从数据中提取信息。
模式匹配自有其定义,同时也有很多种类,这里针对相对复杂的【结构比较】和【数据抽取】进行处理(有时候也叫类型检查与转换)。
直白点说,就是“检查下某个对象,看看是否有我们感兴趣的属性成员,如果有就取出这些成员值供后续使用”。
1、结构比较
考察如下对象
code 01
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
var o = new { a = 2, b = 3, d = 0, c = new { a1 = 7, b1 = 2, e = new { name = "aaa" , Id = 0 } } }; |
当我们明确知道其具体类型时,可以通过属性访问获取相关值,
code 02
1
2
3
|
int r1=o.a; int r2=o.c.a1; string r3=o.c.e.name; |
但是,当 类型不明确 时,比如:
code 03
1
|
method1( object obj) |
在method1中,如何快速方便的获取其相关属性值?
首先,我们知道问题的出现是因为“类型不明确”,那么我们要做的第一件是就是还原类型信息;
在还原类型信息之前,首先要把我们想获取的信息描述出来,以 code 02 为例,
1、希望o上有一个名为a的属性,类型int
2、希望o上有一个名为c的属性,同时c上有一个名为a1的属性, 类型int
3、希望o上有一个名为c的属性,同时c上有一个名为e的属性,同时e上有一个名为name的属性 类型string
。。。。。。
不难发现,a、我们要描述的类型信息不必要与原类型一致,仅表示出期望得到的部分即可;
b、要描述的类型信息中能正确表达层级关系
c、要能够描述所有类型的属性成员
d、明确知道期望的类型信息
e、最好使用语言环境中直接提供的技术手段
综合以上,这里使用匿名对象进行类型描述,简单而且能同时满足以上5点。
code 04
1
2
3
4
5
6
7
8
9
10
11
12
|
var typeinfo = new { a = 3, //default(int) c = new { a1 = 1, e = new { name = default ( string ) } } }; |
注意:类型描述时属性值没有意义,一般可以用default(type),这里使用值是为了后面比对结果。
有了类型描述后,进行类型检查就变的相对简单了,我们以类型描述信息为基准,逐个检查目标对象上有无对应的成员即可。
直接使用反射就可以了。
code 05
1
2
3
4
5
|
if ( pi.Name==npi.Name&& pi.PropertyType == npi.PropertyType) { return true .Result( new GetValue(o => npi.Getter(o))); //扩展方法等见code 06 }<br><br><br> |
code 06
public struct Result<T> { public bool OK; public T Value; public Result(bool ok, T resultOrReason) { this.OK = ok; this.Value = resultOrReason; } public static implicit operator Result<T>(bool value) { return new Result<T>(value, default(T)); } public static explicit operator bool(Result<T> value) { return value.OK; } public static bool operator ==(Result<T> a, Result<T> b) { return a.Equals(b); } public static bool operator !=(Result<T> a, Result<T> b) { return !a.Equals(b); } public override bool Equals(object obj) { var r = (Result<T>)obj; return this.OK == r.OK && object.Equals(this.Value, r.Value); } public override int GetHashCode() { return this.OK.GetHashCode() + (this.Value == null ? 0 : this.Value.GetHashCode()); } }
委托://返回实例上所有筛选值 public delegate IEnumerable<object> GetAllValues(object instance); //返回实例上某个值 public delegate object GetValue(object instance);
//扩展方法 //bool +结果 public static Result<Value> Result<Value>(this bool state, Value value) { return new Result<Value>(state, value); } //属性取值, 反射 public static object Getter(this PropertyInfo info, object instance) { return info.GetValue(instance); } //新实例,反射 public static object New(this Type t, params object[] args) { return args.IsEmpty() ? Activator.CreateInstance(t) : Activator.CreateInstance(t, args); }
考虑到结构会出现嵌套情况,主要代码下:
code 07
1 public static Result<GetAllValues> MatchType(this Type pattern, Type target) { 2 var pis = pattern.GetProperties(); 3 var tpis = target.GetProperties(); 4 if (pis.Length < tpis.Length) 5 { 6 7 var fac = new List<GetValue>(); 8 for (int i = 0; i < pis.Length; i++) 9 { 10 var pi = pis[i]; 11 var r = pi.MatchProp(tpis); 12 if (r.OK) 13 { 14 fac.Add(r.Value); 15 continue; 16 } 17 return false; 29 } 30 return true.Result(new GetAllValues(o => fac.Select(c => c(o)))); 31 } 32 return false; 33 } 34 static Result<GetValue> MatchProp(this PropertyInfo pi, IEnumerable<PropertyInfo> target) { 35 36 var npi = target.FirstOrDefault(c => c.Name == pi.Name)??(pi.Name=="_"?target.FirstOrDefault(c=>c.PropertyType==pi.PropertyType):null); 37 if (npi != null) { 38 if (pi.PropertyType.IsAnonymous() ) 39 { 40 var r = pi.PropertyType.MatchType(npi.PropertyType); 41 if (r.OK) { 42 return true.Result(new GetValue(o => pi.PropertyType.New(r.Value(npi.Getter(o)).ToArray()))); 43 } 44 } 45 else if ( pi.PropertyType == npi.PropertyType) 46 { 47 return true.Result(new GetValue(o => npi.Getter(o))); 48 49 } 50 } 51 return false; 52 53 }
代码说明:
属性使用 名称+属性类型进行检查
如果类型描述中出现 匿名类型 属性(line:38) ,进行层级检查
属性名称为'_' 时忽略属性名,即 匹配第一个类型相等的属性(仅指明一种检查扩展方式: 可以通过属性信息进行特殊处理)
匹配成功后返回 针对目标对象的取值函数
2、目标值抽取
c#中无法方便的动态定义变量,因此,结构检查完成,返回的结果为{true/false,取值函数} (Result<GetAllValues>)。
考虑使用方便,抽取值需要以友好的方式提供给使用者,这里直接创建结构描述类型(匿名类型)的新实例作为返回结果
借助泛型
public static Result<TResult> AsPattern<TPattern, TResult>(this TPattern pattern, object matchobj, Func<TPattern, TResult> then) { var matchType = matchobj.GetType(); var patternType = typeof(TPattern); var matchResult = patternType.MatchType(matchType); if (matchResult.OK) { var patternInstance = patternType.New(matchResult.Value(matchobj).ToArray()); return true.Result(then((TPattern)patternInstance)); } return false; }
调用:
1 var result =typeinfo.AsPattern(o, (c) => c).Value;//result 类型为code 04中typeinfo 的类型 2 //result.a; 3 //result.c.a1; 4 //result.c.e.name;
3、多个模式匹配及方法匹配:
单个模式处理完成后, 多个模式处理 就是简单的集合化。
方法匹配:如果需要在c#中也可以很方便的进行(无ref out 方法),慎用。
1、使用匿名委托描述方法:new {test=default(func<string,object>)} =》期望一个名称为test,参数string,返回object的方法
2、首先检查属性:在目标中检查有无 名称为 test,类型为func<string,object> 的属性,如不存在,则在目标方法中查找
关键代码
方法签名判断
public static bool SignatureEqual(this MethodInfo mi, Type retType, IEnumerable<Type> paramTypes) { return mi.ReturnType == retType && paramTypes.SequenceEqual(mi.GetParameters().Select(p => p.ParameterType)); } //方法与委托类型的参数和返回值是否一致 public static bool SignatureEqual(this MethodInfo mi, Type delegateType) { var cmi = delegateType.GetMethod("Invoke"); return mi.SignatureEqual(cmi); } public static bool SignatureEqual(this MethodInfo mi, MethodInfo nmi) { return mi.SignatureEqual(nmi.ReturnType, nmi.GetParameters().Select(p => p.ParameterType)); }
签名一致后,返回方法调用
new GetValue(o => m.CreateDelegate(pi.PropertyType, o))//m MethodInfo
匹配完成后 直接通过 result.test("aaa")即可调用
走进 LINQ 的世界
序
在此之前曾发表过三篇关于 LINQ 的随笔:
进阶:《LINQ 标准查询操作概述》(强烈推荐)
技巧:《Linq To Objects - 如何操作字符串》 和 《Linq To Objects - 如何操作文件目录》
现在,自己打算再整理一篇关于 LINQ 入门的随笔,也是图文并茂的哦。
目录
LINQ 简介
语言集成查询 (LINQ) 是 Visual Studio 2008 和 .NET Framework 3.5 版中引入的一项创新功能。
传统上,针对数据的查询都是以简单的字符串表示,而没有编译时类型检查或 IntelliSense 支持。此外,您还必须针对以下各种数据源学习一种不同的查询语言:SQL 数据库、XML 文档、各种 Web 服务等等。 通过LINQ, 您可以使用语言关键字和熟悉的运算符针对强类型化对象集合编写查询。
一、介绍 LINQ 查询
查询是一种从数据源检索数据的表达式。随着时间的推移,人们已经为各种数据源开发了不同的语言;例如,用于关系数据库的 SQL 和用于 XML 的 XQuery。因此,开发人员不得不针对他们必须支持的每种数据源或数据格式而学习新的查询语言。LINQ 通过提供一种跨数据源和数据格式使用数据的一致模型,简化了这一情况。在 LINQ 查询中,始终会用到对象。可以使用相同的编码模式来查询和转换 XML 文档、SQL 数据库、ADO.NET 数据集、.NET 集合中的数据以及对其有 LINQ 提供程序可用的任何其他格式的数据。
1.1 查询操作的三个部分
操作三部曲:①取数据源 ②创建查询 ③执行查询
1 internal class Program 2 { 3 private static void Main(string[] args) 4 { 5 //1.获取数据源 6 var nums = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; 7 8 //2.创建查询 9 var numQuery = 10 from num in nums 11 where (num % 2) == 0 12 select num; 13 14 //3.执行查询 15 foreach (var num in numQuery) 16 { 17 Console.WriteLine("{0}", num); 18 } 19 } 20 }
下图显示了完整的查询操作。在 LINQ 中,查询的执行与查询本身截然不同;换句话说,查询本身指的是只创建查询变量,不检索任何数据。
1.2 数据源
在上一个示例中,由于数据源是数组,因此它隐式支持泛型 IEnumerable<T> 接口。支持 IEnumerable<T> 或派生接口(如泛型 IQueryable<T>)的类型称为可查询类型。
//从 XML 中创建数据源 //using System.Xml.Linq; var contacts = XElement.Load(@"c:\xxx.xml");
在 LINQ to SQL 中,首先需要创建对象关系映射。 针对这些对象编写查询,然后由 LINQ to SQL 在运行时处理与数据库的通信。
1 var db = new Northwnd(@"c:\northwnd.mdf"); 2 3 //查询在伦敦的客户 4 var custQuery = 5 from cust in db.Customers 6 where cust.City == "London" 7 select cust;
1.3 查询
查询指定要从数据源中检索的信息。 查询还可以指定在返回这些信息之前如何对其进行排序、分组和结构化。 查询存储在查询变量中,并用查询表达式进行初始化。
1.4 查询执行
1.延迟执行
如前所述,查询变量本身只是存储查询命令。 实际的查询执行会延迟到在 foreach 语句中循环访问查询变量时发生。 此概念称为“延迟执行”。
2.强制立即执行
对一系列源元素执行聚合函数的查询必须首先循环访问这些元素。Count、Max、Average 和 First 就属于此类查询。由于查询本身必须使用 foreach 以便返回结果,因此这些查询在执行时不使用显式 foreach 语句。另外还要注意,这些类型的查询返回单个值,而不是 IEnumerable 集合。
1 var numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; 2 3 var evenNumQuery = 4 from num in numbers 5 where (num % 2) == 0 6 select num; 7 8 var evenNumCount = evenNumQuery.Count();
若要强制立即执行任意查询并缓存其结果,可以调用 ToList<TSource> 或 ToArray<TSource> 方法。
1 var numQuery2 = 2 (from num in numbers 3 where (num % 2) == 0 4 select num).ToList(); 5 6 var numQuery3 = 7 (from num in numbers 8 where (num % 2) == 0 9 select num).ToArray();
此外,还可以通过在紧跟查询表达式之后的位置放置一个 foreach 循环来强制执行查询。但是,通过调用 ToList 或 ToArray,也可以将所有数据缓存在单个集合对象中。
二、基本 LINQ 查询操作
2.1 获取数据源:from
在 LINQ 查询中,第一步是指定数据源。像在大多数编程语言中一样,必须先声明变量,才能使用它。在 LINQ 查询中,最先使用 from 子句的目的是引入数据源和范围变量。
1 //queryAllCustomers 是 IEnumerable<Cutsomer> 类型 2 //数据源 (customers) 和范围变量 (cust) 3 var queryAllCustomers = from cust in customers 4 select cust;
范围变量类似于 foreach 循环中的迭代变量,但在查询表达式中,实际上不发生迭代。执行查询时,范围变量将用作对 customers 中的每个后续元素的引用。因为编译器可以推断 cust 的类型,所以您不必显式指定此类型。
2.2 筛选:where
也许最常用的查询操作是应用布尔表达式形式的筛选器。此筛选器使查询只返回那些表达式结果为 true 的元素。使用 where 子句生成结果。实际上,筛选器指定从源序列中排除哪些元素。
1 var queryLondonCustomers = from cust in customers 2 where cust.City = "London" 3 select cust;
您可以使用熟悉的 C# 逻辑 AND(&&)和 OR(||) 运算符来根据需要在 where 子句中应用任意数量的筛选表达式。
where cust.City = "London" && cust.Name = "Devon"
where cust.City = "London" || cust.Name = "Paris"
2.3 排序:orderby
通常可以很方便地将返回的数据进行排序。orderby 子句将使返回的序列中的元素按照被排序的类型的默认比较器进行排序。
1 var queryLondonCustomers = from cust in customers 2 where cust.City = "London" 3 orderby cust.Name descending 4 select cust;
因为 Name 是一个字符串,所以默认比较器执行从 A 到 Z 的字母排序。若要按相反顺序(从 Z 到 A)对结果进行排序,请使用 orderby…descending 子句。
2.4 分组:group
使用 group 子句,您可以按指定的键分组结果。
1 var queryLondonCustomers = from cust in customers 2 group cust by cust.City; 3 4 foreach (var queryLondonCustomer in queryLondonCustomers) 5 { 6 Console.WriteLine(queryLondonCustomer.Key); 7 foreach (var cust in queryLondonCustomer) 8 { 9 Console.WriteLine(cust.Name); 10 } 11 }
在本例中,cust.City 是键。
在使用 group 子句结束查询时,结果采用列表的列表形式。列表中的每个元素是一个具有 Key 成员及根据该键分组的元素列表的对象。在循环访问生成组序列的查询时,您必须使用嵌套的 foreach 循环。外部循环用于循环访问每个组,内部循环用于循环访问每个组的成员。
如果您必须引用组操作的结果,可以使用 into 关键字来创建可进一步查询的标识符。
1 //custQuery 是 IEnumable<IGrouping<string, Customer>> 类型 2 var custQuery = from cust in customers 3 group cust by cust.City 4 into custGroup 5 where custGroup.Count() > 2 6 orderby custGroup.Key 7 select custGroup;
2.5 联接:join
联接运算创建数据源中没有显式建模的序列之间的关联。例如,您可以执行联接来查找位于同一地点的所有客户和经销商。在 LINQ 中,join 子句始终针对对象集合而非直接针对数据库表运行。
1 var innerJoinQuery = from cust in customers 2 join dist in distributors on cust.City equals dist.City 3 select new {CustomerName = cust.Name, DistributorName = dist.Name};
在 LINQ 中,join 子句始终针对对象集合而非直接针对数据库表运行。
在 LINQ 中,您不必像在 SQL 中那样频繁使用 join,因为 LINQ 中的外键在对象模型中表示为包含项集合的属性。
from order in Customer.Orders...
2.6 选择(投影):select
select 子句生成查询结果并指定每个返回的元素的“形状”或类型。
例如,您可以指定结果包含的是整个 Customer 对象、仅一个成员、成员的子集,还是某个基于计算或新对象创建的完全不同的结果类型。当 select 子句生成除源元素副本以外的内容时,该操作称为“投影”。
三、使用 LINQ 进行数据转换
语言集成查询 (LINQ) 不仅可用于检索数据,而且还是一个功能强大的数据转换工具。通过使用 LINQ 查询,您可以将源序列用作输入,并采用多种方式修改它以创建新的输出序列。您可以通过排序和分组来修改该序列,而不必修改元素本身。但是,LINQ 查询的最强大的功能是能够创建新类型。这一功能在 select 子句中实现。 例如,可以执行下列任务:
3.1 将多个输入联接到一个输出序列
1 class Student 2 { 3 public string Name { get; set; } 4 5 public int Age { get; set; } 6 7 public string City { get; set; } 8 9 public List<int> Scores { get; set; } 10 } 11 12 class Teacher 13 { 14 public int Id { get; set; } 15 16 public string Name { get; set; } 17 18 public int Age { get; set; } 19 20 public string City { get; set; } 21 22 }
1 internal class Program 2 { 3 private static void Main(string[] args) 4 { 5 //创建第一个数据源 6 var students = new List<Student>() 7 { 8 new Student() 9 { 10 Age = 23, 11 City = "广州", 12 Name = "小C", 13 Scores = new List<int>(){85,88,83,97} 14 }, 15 new Student() 16 { 17 Age = 18, 18 City = "广西", 19 Name = "小明", 20 Scores = new List<int>(){86,78,85,90} 21 }, 22 new Student() 23 { 24 Age = 33, 25 City = "梦里", 26 Name = "小叁", 27 Scores = new List<int>(){86,68,73,97} 28 } 29 }; 30 31 //创建第二个数据源 32 var teachers = new List<Teacher>() 33 { 34 new Teacher() 35 { 36 Age = 35, 37 City = "梦里", 38 Name = "啵哆" 39 }, 40 new Teacher() 41 { 42 Age = 28, 43 City = "云南", 44 Name = "小红" 45 }, 46 new Teacher() 47 { 48 Age = 38, 49 City = "河南", 50 Name = "丽丽" 51 } 52 }; 53 54 //创建查询 55 var peopleInDreams = (from student in students 56 where student.City == "梦里" 57 select student.Name) 58 .Concat(from teacher in teachers 59 where teacher.City == "梦里" 60 select teacher.Name); 61 62 //执行查询 63 foreach (var person in peopleInDreams) 64 { 65 Console.WriteLine(person); 66 } 67 68 Console.Read(); 69 } 70 }
3.2 选择各个源元素的子集
1. 若要只选择源元素的一个成员,请使用点运算。
1 var query = from cust in Customers 2 select cust.City;
2. 若要创建包含源元素的多个属性的元素,可以使用具有命名对象或匿名类型的对象初始值设定项。
1 var query = from cust in Customer 2 select new {Name = cust.Name, City = cust.City};
3.3 将内存中的对象转换为 XML
1 //创建数据源 2 var students = new List<Student>() 3 { 4 new Student() 5 { 6 Age = 18, 7 Name = "小A", 8 Scores = new List<int>() {88,85,74,66 } 9 }, 10 new Student() 11 { 12 Age = 35, 13 Name = "小B", 14 Scores = new List<int>() {88,85,74,66 } 15 }, 16 new Student() 17 { 18 Age = 28, 19 Name = "小啥", 20 Scores = new List<int>() {88,85,74,66 } 21 } 22 }; 23 24 //创建查询 25 var studentsToXml = new XElement("Root", 26 from student in students 27 let x = $"{student.Scores[0]},{student.Scores[1]},{student.Scores[2]},{student.Scores[3]}" 28 select new XElement("student", 29 new XElement("Name", student.Name), 30 new XElement("Age", student.Age), 31 new XElement("Scores", x)) 32 ); 33 34 //执行查询 35 Console.WriteLine(studentsToXml);
3.4 对源元素执行操作
输出序列可能不包含源序列的任何元素或元素属性。输出可能是通过将源元素用作输入参数计算出的值的序列。
1 //数据源 2 double[] radii = {1, 2, 3}; 3 4 //创建查询 5 var query = from radius in radii 6 select $"{radius * radius * 3.14}"; 7 8 //执行查询 9 foreach (var i in query) 10 { 11 Console.WriteLine(i); 12 }
【备注】$"{radius * radius * 3.14}" 相当于 string.Format("{0}",radius * radius * 3.14),这里采用的是 C# 6.0 的语法。
四、LINQ 查询操作的类型关系
LINQ 查询操作在数据源、查询本身及查询执行中是强类型的。查询中变量的类型必须与数据源中元素的类型和 foreach 语句中迭代变量的类型兼容。强类型可以保证在编译时捕获类型错误,以便及时改正。
4.1 不转换源数据的查询
下图演示不对数据执行转换的 LINQ to Objects 查询操作。源包含一个字符串序列,查询输出也是一个字符串序列。
①数据源的类型参数决定范围变量的类型。
②选择的对象的类型决定查询变量的类型。此处的 name 为一个字符串。因此,查询变量是一个 IEnumerable<字符串>。
③在 foreach 语句中循环访问查询变量。因为查询变量是一个字符串序列,所以迭代变量也是一个字符串。
4.2 转换源数据的查询
下图演示对数据执行简单转换的 LINQ to SQL 查询操作。查询将一个 Customer 对象序列用作输入,并只选择结果中的 Name 属性。因为 Name 是一个字符串,所以查询生成一个字符串序列作为输出。
①数据源的类型参数决定范围变量的类型。
②select 语句返回 Name 属性,而非完整的 Customer 对象。因为 Name 是一个字符串,所以 custNameQuery 的类型参数是 string,而非Customer。
③因为 custNameQuery 是一个字符串序列,所以 foreach 循环的迭代变量也必须是 string。
下图演示另一种转换。select 语句返回只捕获原始 Customer 对象的两个成员的匿名类型。
①数据源的类型参数始终为查询中的范围变量的类型。
②因为 select 语句生成匿名类型,所以必须使用 var 隐式类型化查询变量。
③因为查询变量的类型是隐式的,所以 foreach 循环中的迭代变量也必须是隐式的。
4.3 让编译器推断类型信息
您也可以使用关键字 var,可用于查询操作中的任何局部变量。但是,编译器为查询操作中的各个变量提供强类型。
五、LINQ 中的查询语法和方法语法
我们编写的 LINQ 查询语法,在编译代码时,CLR 会将查询语法转换为方法语法。这些方法调用标准查询运算符的名称类似 Where、Select、GroupBy、Join、Max和 Average,我们也是可以直接使用这些方法语法的。
查询语法和方法语法语义相同,但是,许多人员发现查询语法更简单、更易于阅读。某些查询必须表示为方法调用。例如,必须使用方法调用表示检索元素的数量与指定的条件的查询。还必须使用方法需要检索元素的最大值在源序列的查询。System.Linq 命名空间中的标准查询运算符的参考文档通常使用方法语法。
5.1 标准查询运算符扩展方法
1 static void Main(string[] args) 2 { 3 var nums = new int[4] { 1, 2, 3, 4 }; 4 5 //创建查询表达式 6 var qureyNums = from n in nums 7 where n % 2 == 0 8 orderby n descending 9 select n; 10 11 Console.WriteLine("qureyNums:"); 12 foreach (var n in qureyNums) 13 { 14 Console.WriteLine(n); 15 } 16 17 //使用方法进行查询 18 var queryNums2 = nums.Where(n => n % 2 == 0).OrderByDescending(n => n); 19 20 Console.WriteLine("qureyNums2:"); 21 foreach (var n in queryNums2) 22 { 23 Console.WriteLine(n); 24 } 25 26 Console.Read(); 27 }
两个示例的输出是相同的。您可以看到两种形式的查询变量的类型是相同的:IEnumerable<T>。
若要了解基于方法的查询,让我们进一步地分析它。注意,在表达式的右侧,where 子句现在表示为对 numbers 对象的实例方法,在您重新调用该对象时其类型为 IEnumerable<int>。如果您熟悉泛型 IEnumerable<T> 接口,那么您就会了解,它不具有 Where 方法。但是,如果您在 Visual Studio IDE 中调用 IntelliSense 完成列表,那么您不仅将看到 Where 方法,而且还会看到许多其他方法,如 Select、SelectMany、Join 和Orderby。下面是所有标准查询运算符。
尽管看起来 IEnumerable<T> 似乎已被重新定义以包括这些附加方法,但事实上并非如此。这些标准查询运算符都是作为“扩展方法”实现的。
5.2 Lambda 表达式
在前面的示例中,通知该条件表达式 (num % 2 == 0) 是作为内联参数。Where 方法:Where(num => num % 2 == 0) 此内联表达式称为lambda 表达式。将代码编写为匿名方法或泛型委托或表达式树是一种便捷的方法,否则编写起来就要麻烦得多。=> 是 lambda 运算符,可读为“goes to”。运算符左侧的 num 是输入变量,与查询表达式中的 num 相对应。编译器可推断 num 的类型,因为它了解 numbers 是泛型 IEnumerable<T> 类型。lambda 表达式与查询语法中的表达式或任何其他 C# 表达式或语句中的表达式相同;它可以包括方法调用和其他复杂逻辑。“返回值”就是表达式结果。
5.3 查询的组合性
在上面的代码示例中,请注意 OrderBy 方法是通过在对 Where 的调用中使用点运算符来调用的。Where 生成筛选序列,然后 Orderby 通过对该序列排序来对它进行操作。因为查询会返回 IEnumerable,所以您可通过将方法调用链接在一起,在方法语法中将这些查询组合起来。这就是在您通过使用查询语法编写查询时编译器在后台所执行的操作。并且由于查询变量不存储查询的结果,因此您可以随时修改它或将它用作新查询的基础,即使在执行它后。
传送门
入门:《走进 LINQ 的世界》
进阶:《LINQ 标准查询操作概述》(强烈推荐)
技巧:《Linq To Objects - 如何操作字符串》 和 《Linq To Objects - 如何操作文件目录》
本文首联:http://www.cnblogs.com/liqingwen/p/5832322.html
【参考】https://msdn.microsoft.com/zh-cn/library/bb397897(v=vs.100).aspx 等
【来源】本文引用部分微软官方文档的图片
移除Excel工作表密码保护小工具含C#源代码
2018-11-07 19:44 by zhoujie, 61 阅读, 0 评论, 收藏, 编辑
有朋友发了个Excel.xlsx文件给我,让我帮忙看看里面是怎么做出来的。打开审阅后发现,每个Excel工作表都添加了密码保护:
看不到里面的隐藏列和公式等等,感觉很神秘。于是研究了一下Excel文件的格式,做了一个解除工作表密码的小程序:
原理很简单:.
xlsx文件其实是一个zip压缩文件,而每个文件都是xml格式。微软专门提供了SDK,我是直接用DotNetZip操作的,移除每个工作表的加密节点即可。
腾讯QQ会员中心g_tk32算法【C#版】
最近用C#写qq活动辅助类程序,碰到了会员签到的gtk算法不一样,后来网上找了看,发现有php版的(https://www.oschina.net/code/snippet_1378052_48831)
后来参考了php版的查php相关的资料用C#写了一个:
/// <summary> /// 计算gtk32值 /// </summary> /// <param name="skey"></param> /// <returns></returns> public static string GetGTK32(string skey) { var hash = 5381; var md5Key = "tencentQQVIP123443safde&!%^%1282"; var start = hash << 5; var result = string.Empty; for (int i = 0; i < skey.Length; i++) { var ascode = CharToASCII(skey.Substring(i, 1)); result += (hash << 5) + ascode; hash = ascode; } var str = start + (result + md5Key); return GetMD5(str); } /// <summary> /// MD5加密 /// </summary> /// <param name="text"></param> /// <returns></returns> public static string GetMD5(string text) { StringBuilder sb = new StringBuilder(); using (MD5 md5 = MD5.Create()) { byte[] md5Byte = md5.ComputeHash(Encoding.Default.GetBytes(text)); for (int i = 0; i < md5Byte.Length; i++) { sb.Append(md5Byte[i].ToString("x2")); } } return sb.ToString(); } /// <summary> /// /*字符转化为ASCII*/ /// </summary> /// <param name="character"></param> /// <returns></returns> static int CharToASCII(string character) { ASCIIEncoding asciiEncoding = new ASCIIEncoding(); int intAsciiCode = asciiEncoding.GetBytes(character)[0]; return intAsciiCode; }
另附上解析cookie中的skey和p_skey方法和gtk算法:
/// <summary> /// 解析cookie,取到Skey /// </summary> /// <param name="cookies">腾讯QQ cookie</param> /// <returns></returns> public static string GetSkey(string cookies) { #region 字符串分割解析 //var keyStr = "skey="; //var index = cookies.IndexOf(keyStr) + keyStr.Length; //var skey = cookies.Remove(0, index); //if (skey.Contains(";") && skey.Length > 10) //{ // var laindex = cookies.IndexOf(";"); // skey = skey.Remove(10); //} #endregion var skey = Regex.Match(cookies, "skey=(.){10}?").Value.Remove(0, 5); if (skey.Length > 10) { skey.Remove(10); } return skey; } /// <summary> /// 解析cookie,取到p_skey /// </summary> /// <param name="cookies">腾讯QQ cookie</param> /// <returns></returns> public static string Getp_skey(string cookies) { return Regex.Match(cookies, "p_skey=(.)+?_").Value.Remove(0, 7); } /// <summary> /// 算出g_tk /// </summary> /// <param name="sKey">cookie中的sKey值</param> /// <returns></returns> public static string GetGTK(string sKey) { var hash = 5381; for (int i = 0, len = sKey.Length; i < len; ++i) { hash += (hash << 5) + sKey[i]; } return (hash & 0x7fffffff).ToString(); }
C#7.2——编写安全高效的C#代码 c# 中模拟一个模式匹配及匹配值抽取 走进 LINQ 的世界 移除Excel工作表密码保护小工具含C#源代码 腾讯QQ会员中心g_tk32算法【C#版】的更多相关文章
- 移除Excel工作表密码保护小工具含C#源代码
有朋友发了个Excel.xlsx文件给我,让我帮忙看看里面是怎么做出来的.打开审阅后发现,每个Excel工作表都添加了密码保护: 看不到里面的隐藏列和公式等等,感觉很神秘.于是研究了一下Excel文件 ...
- 腾讯QQ会员中心g_tk32算法【C#版】
最近用C#写qq活动辅助类程序,碰到了会员签到的gtk算法不一样,后来网上找了看,发现有php版的(https://www.oschina.net/code/snippet_1378052_48831 ...
- 腾讯QQ会员技术团队:人人都可以做深度学习应用:入门篇(下)
四.经典入门demo:识别手写数字(MNIST) 常规的编程入门有"Hello world"程序,而深度学习的入门程序则是MNIST,一个识别28*28像素的图片中的手写数字的程序 ...
- 腾讯QQ会员技术团队:以手机QQ会员H5加速为例,为你揭开sonic技术内幕
目前移动端越多越多的网页开始H5化,一方面可以减少安装包体积,另一方面也方便运营.但是相对于原生界面而言,H5的慢速问题一定被大家所诟病,针对这个问题,目前手Q存在几种方案,最常见的便是离线包方案,但 ...
- C#7.2——编写安全高效的C#代码
原文地址:https://docs.microsoft.com/zh-cn/dotnet/csharp/write-safe-efficient-code?view=netcore-2.1 值类型的优 ...
- QQ 腾讯QQ(简称“QQ”)是腾讯公司开发的一款基于Internet的即时通信(IM)软件
QQ 编辑 腾讯QQ(简称“QQ”)是腾讯公司开发的一款基于Internet的即时通信(IM)软件.腾讯QQ支持在线聊天.视频通话.点对点断点续传文件.共享文件.网络硬盘.自定义面板.QQ邮箱等多种功 ...
- QQ会员活动运营平台架构设计实践——高效自动化运营
QQ会员活动运营平台(AMS),是QQ会员增值运营业务的重要载体之一,承担海量活动运营的Web系统.在过去四年的时间里,AMS日请求量从200-500万的阶段,一直增长到日请求3-5亿,最高CGI日请 ...
- Flutter实战视频-移动电商-66.会员中心_编写ListTile通用方法
66.会员中心_编写ListTile通用方法 布局List里面嵌套一个ListTile的布局效果 里面有很多条记录,以后可能还会增加,所以这里我们做一个通用的组件 通用组件方法 这里使用Column布 ...
- QQ会员AMS平台PHP7升级实践
作者:徐汉彬链接:https://zhuanlan.zhihu.com/p/21493018来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. QQ会员活动运营平台(AMS ...
随机推荐
- shell常用的系统变量
$#: 命令行参数的个数 $n : 当前程序的第n个参数,n=1,2,-,9 $0: 当前程序的名称 $?: 执行上一个指令或函数的返回值 $*: 以"参数1,参数 ...
- datatables后端分页
0x01 缘由 平时较少涉及前端,这次本以为模板中有表单,分页跳转搜索功能都比较齐全,可以高枕无忧,但是细看模板中的分页跳转是不需要与后台交互的,数据一次性写在前端,再有前端插件完成分页. 这种方式肯 ...
- Ef 自动迁移,日志
Ef 迁移 在vs打开程序控制台 2,选择程序集 ,如果是初次,输入 Enable-Migrations,启动迁徙 3 添加迁移,完成修改 4,之后会自动生成迁移配置文件. 然后再上下文类中加入 两 ...
- SpringBoot学习历程
新年新气象,更新了一下本人所有写的关于SpringBoot的文章目录,感谢大家长期以来的支持,在接下来的日子还会不定期的进行更新. 入门 使用IntelliJ Idea新建SpringBoot项目 S ...
- 大数据环境完全分布式搭建hbase-0.96.2-hadoop2
1.上传hbase安装包 2.解压 3.配置hbase集群,要修改3个文件 (首先zookeeper集群已经安装好了 并且启动 hadoop启动) 注意:要把hadoop的hdfs-site.xml和 ...
- python模拟银行家算法
前言: 大二第一学期学习了操作系统,期末实验课题要求模拟算法.遂根据自己学习的python写下此文.以此锻炼自己编码能力.虽说是重复造轮子,但还是自己的思路体现 代码及注释如下(银行家算法不再赘述): ...
- spring 空指针报错,Could not create connection to database server.
驱动问题,换成最近版本的mysql驱动
- (转)HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别
①HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象.当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算h ...
- BZOJ5177 : [Jsoi2013]贪心的导游
首先预处理出对于每个模数,所有被模数按结果从大到小排序的结果,那么对于一个询问,如果可以在$O(1)$时间内判断某个数字是否出现,则可以$O(1000)$回答. 考虑对序列进行分治,对于区间$[l,r ...
- android Resources 类的使用
使用 R.<resource_type>.<resource_name> 获取的是资源的一个 id (int 类型), 但有时候我们需要获取资源本身,这时候我们可以通过 Res ...