使用Func.docx
- 文档编号:26940927
- 上传时间:2023-06-24
- 格式:DOCX
- 页数:64
- 大小:795.72KB
使用Func.docx
《使用Func.docx》由会员分享,可在线阅读,更多相关《使用Func.docx(64页珍藏版)》请在冰豆网上搜索。
使用Func
使用Func
Func
封装一个具有两个参数并返回 TResult 参数指定的类型值的方法。
语法
publicdelegateTResultFunc
T1arg1,
T2arg2
)
?
类型参数
inT1
此委托封装的方法的第一个参数类型。
该类型参数是逆变。
即可以使用指定的类型或派生程度更低的类型。
有关协变和逆变的更多信息,请参见泛型中的协变和逆变。
inT2
此委托封装的方法的第二个参数类型。
outTResult
此委托封装的方法的返回值类型。
该类型参数是协变。
即可以使用指定的类型或派生程度更高的类型。
有关协变和逆变的更多信息,请参见泛型中的协变和逆变。
参数
arg1类型:
T1
此委托封装的方法的第一个参数。
arg2类型:
T2
此委托封装的方法的第二个参数。
返回值
类型:
TResult
此委托封装的方法的返回值。
备注
可以使用此委托表示一种能以参数形式传递的方法,而不用显式声明自定义委托。
封装的方法必须与此委托定义的方法签名相对应。
也就是说,封装的方法必须具有两个均通过值传递给它的参数,并且必须返回值。
?
若要引用具有两个参数并返回void的方法(或者要在VisualBasic中引用被声明为Sub而不是被声明为Function的方法),请改用泛型Action
在使用 Func
例如,以下代码显式声明了一个名为 ExtractMethod 的委托,并将对ExtractWords 方法的引用分配给其委托实例。
示例
下面的示例演示如何声明和使用 Func
此示例声明一个 Func
如果 String 参数的长度等于 Int32 参数的值,则此lambda表达式将返回 true。
随后在查询中使用封装此方法的委托来筛选字符串数组中的字符串。
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
publicclassFunc3Example
{
publicstaticvoidMain()
{
Func
String[]words={"orange","apple","Article","elephant","star","and"};
IEnumerable
foreach(StringwordinaWords)
Console.WriteLine(word);
}
}
了解完这些以后,我们来看看它的应用。
不知道童鞋们有没有遇到这样的问题,在读取数据访问层中数据集合时,发现该方法需要返回的结果中包括对象及其外键对象时,又不想添加一个实体类来封装它,那么有什么别的好办法吗?
也许您会选择用动态对象(dynamic关键字),不错,这个这确实可以解决这个问题,但是有一个弊端,且不论动态对象在运行时编译,在编写程序时,它存在一个很不方便的体验,它不能点(.)出它的属性,不免产生意外的拼写错误或是寻找-复制-黏贴的麻烦。
那么怎么办呢?
我们可以利用Func
publicIEnumerable
{
varresults=fromuinUser
joinpinPersononu.Idequalsp.uId
selectnew{User=u,Person=p};
IList
foreach(variteminresults.ToList())
{
resultItems.Add(itemFactory(item.User,item.Person));
}
returnresultItems;
}
varquery=userDao.GetAllUser((u,p)=>
{
returnnew{u.Id,u.LoginName,pId=p.Id,p.Name};
});
此外,当然也可以自定义更多的委托,这里笔者就不详细介绍了,读者可自行尝试。
哈哈,这样是否有帮到您呢?
本人文笔粗糙简陋,请多多大虾们指教!
术语解释:
CLR:
公共语言运行时(CommonLanguageRuntime)
FCL:
Framework类库(FrameworkClassLibrary)
IL:
中间语言(IntermediateLanguage)
类型基础
CLR要求所有的类型最终都是从System.Object派生的。
这符合在面向对象的语言和设计中,一切皆为对象!
从图中,我们可以看到,显式从System.Object派生的类ExplicitlyDerivedFromObject和没有显式指定其父类的类ImplicitlyDerivedFromObject,最终生成的IL中间都是一致的。
如果我们没有指定一个类的父类,编译器会自动为我们的类加上System.Object父类。
基元类型
编译器直接支持的数据类型称为基元类型(Primitivetype),每种基元类型都对应FCL中的某一类型。
如下表格所示每种基元类型与对应的FCL类型:
我们也可以简单理解成,编译器会自动在我们的每个源码文件中,加上以下using指令(using在此的作用是给类型起一个别名):
usingint=System.Int32;
usingstring=System.String;
……
理解了这一点,我们应该能知道在我们的代码中,写int还是Int32,用string还是用String本质上是一样的!
从下面两种不同的写法生成的IL代码来看,结果也是符合预计的。
我们还应该知道,C#中的int永远是代表32位整型,long永远是64位整型等。
这一点和其他编程语言可能是不一致的(比如C/C++可能会根据机器平台决定,比如int在16位机器上可能是16位,而在32位机器上可能是32位。
对于C/C++本人早已记忆模糊,这里如果有错误,请指出!
)。
引用类型、值类型
CLR支持两种类型:
引用类型和值类型,它们的区别是在内存分配方式上的差异:
引用类型是从托管堆上分配的;值类型是在线程栈上分配的。
而CLR的垃圾回收是针对托管堆的,因此值类型不受垃圾回收器的控制。
在FCL中,所有称为“结构”(struct)的类型都是值类型,所有称为“类”(class)的类型都是引用类型。
所有的Struct都直接派生自抽象类System.ValueType,而System.ValueType直接从System.Object派生。
所有的枚举都直接从System.Enum派生,而后者又派生自System.ValueType,所以枚举也是值类型。
由于CLR的单继承规则,所以我们在定义值类型时,不能指定基类型,但可以实现接口。
同时从下图生成的IL也可以看出,值类型是隐式密封的(sealed),也就是说也不能从值类型派生。
虽然引用类型与值类型实质只是内存分配上的差异,但这种差异会导致两种类型在行为表现上有着明显不同,比如下面的例子:
structValType{publicintx;}
classRefType{publicintx;}
classProgram
{
staticvoidMain(string[]args)
{
ValTypev1=newValType();//在栈上分配内存
RefTyper1=newRefType();//在堆上分配内存
v1.x=2;
r1.x=2;
//执行到这里,内存结构请见图1
Console.WriteLine(v1.x);//2
Console.WriteLine(r1.x);//2
ValTypev2=v1;//在栈上分配内存(v2),并把v1栈的内容复制到v2
RefTyper2=r1;//把r1的堆地址复制给r2
v2.x=5;//只改变v2栈的内容
r2.x=5;//由于r2和r1都引用同一个堆上的对象,改变r2也会改变r1
//执行到这里,内存结构请见图2
Console.WriteLine(v1.x);//2
Console.WriteLine(r1.x);//5注意这里变成了r2修改后的值
Console.WriteLine(v2.x);//5
Console.WriteLine(r2.x);//5
Console.ReadKey();
}
}
首先我们定义一个一值类型与一个引用类型,内部都只有一个字段。
用new操作符分配内存时,值类型v1的内存分配在了线程栈上,引用类型r1的内存分配在了托管堆上,在程序运行到第一次WriteLine输出时,看到的结果是一致的。
但接下来声明两个新的对象并执行赋值时,这里的发生的事明显不同:
虽然赋值操作都是拷贝线程栈上变量的内容,但由于值类型变量v1的栈内容就是ValType类型实例本身,而引用类型r1的栈内容是RefType对象实例在堆上的地址。
所以赋值后的结果就是,v1和v2各保存了一份ValType类型实例,而r1和r2保存了同一块堆内存的地址。
所以改变r2对象导致了r1对象的随同改变。
下面是内存示意图:
图1
图2
虽然值类型实例不需要垃圾回收,但由于值类型在传递时,传递的是内容本身,所以并不适合将所一些实例较大的类型定义为值类型。
实现上除非满足以下所有条件,否则不应该将一个类型声明为值类型。
∙没有更改其字段的成员,即该类型是不可变的。
(建议所有字段为readonly)
∙类型不需要从其他任何类型继承。
(值类型不能选择基类)
∙类型也不会派生出其他任何类型。
(所有的值类型都是隐式密封sealed的)
∙实例较小(约<=16Byte)或较大但不作为方法实参传递,也不从方法返回。
值类型的装箱与拆箱
将值类型转换成一个引用类型的过程叫装箱,整个过程看起来是这样的:
1.在托管堆中分配好内存,分配的内存量=值类型的各个字段所需的内存量+所有堆上对象都有的两个额外成员(类型对象指针和同步块索引)所需的内存量。
2.值类型的字段复制到新分配的内存。
3.返回对象的地址。
拆箱仅是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。
虽然拆箱比装箱代价低,但实际在拆箱之后往往紧接着就是赋值操作(内存复制)。
显然装箱和拆箱/复制会对应用程序的速度与内存消耗上产生不利影响,所以应该了解到这一点,并尽量避免装箱和拆箱操作。
那么什么时候会发生装箱和拆箱,最直观的方法就是看生成的IL代码(IL对应指令是分别是box与unbox),比如下面的例子:
示例中ArrayList的Add方法参数是Object类型,也就是说一个引用类型(在堆上分配的内存),当我们传递int类型时,这里便会将int实例装箱,以返回一个堆上的地址。
在将array[0]强制转型为int时,由于值类型int的对象是在线程栈上分配的,所以这里拆箱并紧接着发生赋值(内存复制)操作。
同时为了对比,我加了引用类型的reference,可以看出引用类型是不会发生装箱与拆箱的。
那么如何避免(或减少)装箱与拆箱:
∙尽量使用泛型集合。
∙尽量将装箱与拆箱操作移到循环体之外。
∙定义一个方法如果可接收引用类型或值类型时,尽量不要将参数定义为object,可以考虑通过重载定义多个版本或定义泛型方法。
总结
本文只能算是对自己看书的一点小结,分享出来的目的一是希望如果对某些知识理解有误,能及时得到大家指正;同时如果您感觉这篇博文有一点小价值,那我的第二个目的也就达到了。
在C#中,一个类型内部可以定义多种成员:
常量、字段、实例构造器、类型构造器(静态构造器)、方法、操作符重载、转换操作符、属性、事件、类型。
类型的可见性有public和internal(默认)两种,前者定义的类型对所有程序集中的所有类型都可见,后者定义的类型只对同一程序集内部的所有类型可见:
publicclassPublicClass{}//所有处可见
internalclassExplicitlyInternalClass{}//程序集内可见
classImplicitlyInternalClass{}//程序集内可见(C#编译器默认设置为internal)
成员的可访问性(按限制从大到小排列):
∙Private只能由定义成员的类型或嵌套类型中方法访问
∙Protected只能由定义成员的类型或嵌套类型或派生类型中方法访问
∙Internal 只能由同程序集类型中方法访问
∙Protected Internal 只能由定义成员的类型或嵌套类型或派生类型或同程序集类型中方法访问(注意这里是或的关系)
∙Public 可由任何程序集中任何类型中方法访问
在C#中,如果没有显式声明成员的可访问性,编译器通常默认选择Private(限制最大的那个),CLR要求接口类型的所有成员都是Public访问性,C#编译器知道这一点,因此禁止显式指定接口成员的可访问性。
同时C#还要求在继承过程中派生类重写成员时,不能更改成员的可访问性(CLR并没有作这个要求,CLR允许重写成员时放宽限制)。
静态类
永远不需要实例化的类,静态类中只能有静态成员。
在C#中用static这个关键词定义一个静态类,但只能应用于class,不能应用于struct,因为CLR总是允许值类型实例化。
C#编译器对静态类作了如下限制:
∙静态类必须直接从System.Object派生
∙静态类不能实现任何接口(因为只有使用类的一个实例才能调用类的接口方法)
∙静态类只能定义静态成员(字段、方法、属性、事件)
∙静态类不能作为字段、方法参数或局部变量使用
∙静态类在编译后,会生成一个被标记为abstract和sealed的类,同时编译器不会生成实例构造器(.ctor方法)
分部类、结构和接口
C#编译器提供一个partial关键字,以允许将一个类、结构或接口定义在多个文件里。
在编译时,编译器自动将类、结构或接口的各部分合并起来。
这仅是C#编译器提供的一个功能,CLR对此一无所知。
常量
常量就是代表一恒定数据值的符号,比如我们将圆周率3.12415926定义成名为PI的常量,使代码更容易阅读。
而且常量是在编译时就代入运算的(常量就是一个符号,编译时编译器就会将该符号替换成实际值),不会造成任何性能上的损失。
但这一点也可能会造成一个版本问题,即假如未来修改了常量所代表的值,那么用到此常量的地方都要重新编译(我个人认为这也是常量名称的由来,我们应该将恒定不变的值定义为常量,以免后期改动时产生版本问题)。
下面的示例也验证了这一点,Test1和Test2方法内部的常量运算在编译后,就已经运算完成。
从上面示例,我们还能看出一点:
常量key和value编译后是静态成员,这是因为常量通常与类型关联而不是与实例关联,从逻辑上说,常量始终是静态成员。
但对于在方法内部定义的常量,由于作用域的限制,不可能有方法之外的地方引用到这个常量,所以在编译后,常量被优化了。
字段
字段是一种数据成员,在OOP的设计中,字段通常是用来封装一个类型的内部状态,而方法表示的是对这些状态的一些操作。
在C#中字段可用的修饰符有
∙Static 声明的字段与类型关联,而不是与对象关联(默认情况下字段与对象关联)
∙Readonly 声明的字段只能在构造器里写入值(可以通过反射修改)
∙Volatile 声明的字段为易失字段(用于多线程环境)
这里要注意的是将一个字段标记为readonly时,不变的是引用,而不是引用的值。
示例:
classReadonlyField
{
//chars保存的是一个数组的引用
publicreadonlychar[]chars=newchar[]{'A','B','C'};
voidMain()
{
//以下改变数组内存,可以改成功
chars[0]='X';
chars[1]='Y';
chars[2]='Z';
//以下更改chars引用,无法通过编译
chars=newchar[]{'X','Y','Z'};
}
}
属性
CLR支持两种属性:
无参属性和有参属性(C#中称为索引器)。
面向对象设计和编程的重要原则之一就是数据封装,这意味着字段(封装对象的内部状态)永远不应该公开。
因此,CLR提供属性机制来访问字段内容(VS中输入propfull加两次Tab会为我们自动生成字段和属性的代码片断)。
下面的示例中,Person对象内部有一个表示年龄的字段,如果直接公开这个字段,则不能保存外部不会将age设置为0或1000,这显然是没有意义的(也破坏了数据封装性),所以通过属性,可以在操作字段时,加一些额外逻辑,以保证数据的有效性。
classPerson
{
//Person对象的内部状态
privateintage;
//用属性来安全地访问字段
publicintAge
{
get{returnage;}
set
{
if(value>0&&value<=150)age=value;
else{}//抛出异常
}
}
}
编译上述代码后,实际上编译器会将属性内的get和set访问器各生成一个方法,方法名称是get_和set_加上属性名,所以说属性的本质是方法。
如果只是为了封装一个字段而创建一个属性,C#还为我们提供了一种更简单的语法,称为自动实现的属性(AIP)。
下面是一个示例(在VS中输入prop加两次TAB会为我们生成AIP片断):
这里要注意一点,由于AIP的支持字段是编译器自动生成的,而且编译器每次编译都可能更改这个名称。
所以在任何要序列化和反序列化的类型中,都不要使用AIP功能。
对象和集合初始化器
在实现编程中,我们经常构造一个对象,然后设置对象的一些公共属性或字段。
为此C#为我们提供了一种简化的语法来完成这些操作。
如下示例:
classPerson
{
//AIP
publicstringName{get;set;}
publicintId{get;set;}
publicintAge{get;set;}
voidMain()
{
//没有使用对象初始化器的语法
Personp1=newPerson();
p1.Id=1;
p1.Name="Heku";
p1.Age=24;
//使用对象初始化器的语法
Personp2=newPerson(){Id=1,Name="Heku",Age=24};
}
}
使用对象初始化器的语法时,实际上编译器为我们生成的代码和上面是一致的,但是下面的代码明显更加简洁。
如果本来就是要调用类型的无参构造器,C#还允许我们省略大括号之前的小括号:
Personp2=newPerson{Id=1,Name="Heku",Age=24};
如果一个属性的类型实现了IEnumerable或IEnumerable
比如我们在上例中的Person类中加入一个新属性Skills
publicList
然后可以用下面的语法来初始化
//使用简化的对象初始化器语法+简化集合初始化器语法
Personp3=newPerson{Id=1,Name="heku",Age=24,Skills=newList
这里我们用newList
编译器会我们生成的代码看起来是这样的:
p3.Skills=newList
p3.Skills.Add("C#");
p3.Skills.Add("jQuery");
匿名类型
有时候,我们需要封装一组数据,只有属性或字段,没有方法,并且只用于当前程序,不在项目间重用。
如果按传统做法,我们需要手工定义一个类来封装这组数据。
匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。
如下示例:
从上面示例可以看到,我们只写了一句
varobj=new{Id=1,Name="Heku",Addr="China"};
编译器会为我们做大量的工作,首先为我们定义一个类型(类型名称是编译器编译时才生成的,编程时还不知道,所以叫匿名类型),类型内部包含了三个属性Id、Name、Addr以及对应的支持字段,同时也生成了三个get访问器方法,没有生成set访问器方法(说明匿名类型的属性是只读的)。
另外在Main方法中,由于我们编程过程中,并不知道类型名称,所以必须用var关键字来让编译器自行推荐类型(虽然也可以用object或dynamic,但这完全没有意义)。
通过查看Main编译后的IL代码我们还可以发现,匿名类型的初始化是通过调用匿名类型的有参构造器来完成的,这点与之前也不相同(因为匿名类型属性是只读的,不能通过调用无参初始化器初始化后再设置属性值,编译器也根本没有生成匿名类型的无参构造器)。
有参属性
前面讲到的属性都没有参数,实现上还有一种可以带参数的属性,称之为有参属性(C#中叫索引器)。
classStringArray
{
privatest
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 使用 Func