无参属性
许多类型都定义了能被获取或更高的状态信息。这种状态信息一般作为类型的字段成员实现。例如一下类型包含两个字段:
public sealed class Employee{ public string name; public int age; }
创建该类型的实例后,可以使用一下形式的代码轻松获取或设置他的状态信息
e.Name=”ascdasdasd”;
这种设置对象状态信息的做法十分常见,但不应该像这样实现。面向对象设计和编程的重要原则之一就是数据封装,意味着类型的字段永远不应该公开,否则很容易因为不恰当使用字段而破坏对象的状态。而且,你可能希望访问字段来执行一些副作用、缓存某些值或者推迟创建一些内部对象。其二,你可能希望以线程安全的方式访问字段。其三,字段可能是一个逻辑字段,他的值不由内存中的字节表示,而是通过某个算法来计算获得。
基于上述原因,强烈建议将所有字段都设为private。要允许用户或类型获取和设置状态信息,就公开一个针对该用途的方法。封装了字段访问的方法通常称为访问器(accessor)方法。访问器方法可选择对数据的合理性进行检查,确保对象的状态用于不被破坏。
public sealed class Employee { public string Name { get; set; } private int _age; public int Age { get { return _age; } set { if (value<0) { throw new ArgumentOutOfRangeException("value",value.ToString(),"greater than 0"); } _age = value; } } }
可将属性想象成智能字段,即背后有额外逻辑的字段。clr支持静态、实例、抽象和虚属性。另外,属性可用任意“可访问性”修饰符来标记,而且可以再接口中定义。
每个属性都有名称和类型(类型不能使void)。属性不能重载,即不能定义名称相同、类型不同的两个属性。定义属性时通常同时制定get和set两个方法。但可省略set方法来定义只读属性,或省略get方法来定义只写属性。
经常利用属性的get和set方法操纵类型中定义的私有字段。私有字段通常称为支持字段。但get和set方法并非一定要访问支持字段。
定义属性时,取决于属性的定义,编译器在最后的托管程序集中生成以下两项或三项。
1 代表属性get访问器的方法。仅在属性定义了get访问器方法时生成。
2 代表属性set访问器的方法。仅在属性定义了set访问器方法时生成。
3 托管程序及元数据中的属性定义,这一项必然生成。
自动实现属性(automatically Implemented Property,Aip)
例如上文定义的
public string Name { get; set; }
声明属性而不提供get、set方法的实现,c#会自动为你声明一个私有字段。
对象和集合初始化器
经常要构造一个对象并设置对象的一些公共属性(或字段)。为了简化这个常见编程模式,c#支持一种特殊的对象初始化语法
Employee e=new Employee(){ Name=”xxxxxx”,Age=45};
这个语句做了好几件事情,包含构造一个Employee对象,调用他的无参构造器,将他的属性赋值。
匿名类型
利用c#的匿名类型功能,可以用很简洁的语法来自动声明不可变(immutable)的元组类型。元组类型是含有一组属性的类型,这些属性通常以某种方式相互关联。在以下代码的第一行,我定义了含有两个属性的类,并赋值。
var o1=new {Name=”jeff”,Year=1999};
第一行代码创建了匿名类型,我没有在new关键字后制定类型名称,所以编译器会自动创建类型名称,而且不会告诉我这个名称具体是什么(这正式匿名的含义)。这行代码使用上一节讨论的“对象初始化器”语法来声明属性,同时初始化这些属性。由于我不知道编译时的类型名称,所以我们使用var,告诉编译器根据赋值操作符=右侧表达式推断类型。最终,编译器生成的类看起来像这样
编译器会生成equeals和gethashcode方法,因此匿名类型的实例能放到哈希表集合中。属性是只读的,而非可读可写,目的是防止对象的哈希码发生改变。如果对象在哈希表中作为键使用,更改它的哈希码会造成再也找不到它。
编译器支持用另外两种语法声明匿名类型中的属性,能根据变量推断属性名和类型:
string Name=“asdasd”; DateTime dt= DateTime.Now; var o2=new { Name , dt.Year}
在这个例子中,编译器判断第一个属性应该叫Name。由于Name是局部变量的名称,所以编译器将属性类型设为与局部变量相同的类型string。对于第二个属性,编译器使用字段./属性的名称:Year。Year是datetime类的一个int属性。
匿名类型经常与Linq配合使用。可用linq执行查询,从而生成由一组对象构成的集合,这些对象都是相同的匿名类型。然后,可以对结果集合中的对象进行处理。
System.Tuple类型
在system命名空间,Microsoft定义了几个泛型tuple类型,他们全部从object派生,区别只在于元数。
有参属性
前面的案例中,属性get访问器方法不接受参数,因此成为无参属性。由于用起来就像访问字段,所以很容易理解。除了这些与字段相似的属性,编程语言还支持有参属性,它的get访问器方法接收一个或多个参数,set访问器方法接受两个或多个参数。另外,不同编程语言对有参属性的称呼也不同。c#称为索引器,visual basic则称为默认属性。
c#使用数组风格的语法来公开有参属性。换句话说,可将索引器看出是C#开发人员对[]操作符的重载。
经常要创建索引器来查询关联数组中的值。system.collections.generic.dictionary类型就提供了这样的一个索引器,它获取一个键,并返回与该键关联的值。和无参属性不同,类型可提供多个重载的索引器,只要这些索引器的签名不同。
调用属性访问器方法时的性能
对于简单的get和set访问器方法,jit编译器会将代码内联(inline,或者说嵌入)。这样一来,使用属性(而不是使用字段)就没有性能上的损失。内联是指将方法(目前说的是访问器方法)的代码直接编译到调用它的方法中。这就避免了运行时发出调用所产生的开销,代价是编译好的方法变得更大。由于属性访问器方法包含的代码一般很少,所以对内联会使生成的本机代码变得更小,而且执行得更快。
注意:jit编译器在调试代码时不会内联属性方法。
属性访问器的可访问性
有时希望为get访问器方法指定一种可访问性,为set访问器方法指定另一种可访问性。最常见的情形是提供 get访问器和受保护set访问器。
private int _age; public int Age { get { return _age; } protected set { if (value<0) { throw new ArgumentOutOfRangeException("value",value.ToString(),"greater than 0"); } _age = value; } }
如上述代码所示,age属性本身声明为public ,意味着get访问器方法时公共的,而set访问器方法声明为只能从类内部定义的代码中调用,或者从派生类的代码中调用。
定义属性时,如果两个访问器方法需要不同的可访问性,c#要求必须为属性本身指定限制最小的可访问性。然后,两个访问器只能选择一个来使用限制较大的。
事件
定义了事件成员的类型允许类型(或类型的实例)通知其他对象发生了特定的事情。例如,button类提供了click事件。应用程序中的一个或者多个对象可接收关于该事件的通知,一遍在button被单击之后采取特定操作。我们用事件这种类型成员来实现这种交互。具体地说,定义了事件成员的类型能提供一下功能。
1、 方法能登记它对事件的关注
2、 方法能注销它对事件的关注
3、 事件发生时,登记了的方法将受到通知。
类型之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表。事件发生后,类型将通知列表中所有已登记的方法。
clr事件模型以委托为基础。委托是调用回调方法的一种类型安全的方式。对象凭借回调方法接收它们订阅的通知。本文涉及委托,但委托的细节会专门在另一篇文章里面讲述。
为了理解事件在clr中的工作机制,我们先来描述事件一个很有用的场景。假定要涉及一个电子邮件应用程序。电子邮件到达时,用户可能希望将该邮件转发给传真机或寻呼机。先设计名为MailManager的类型来接收传入的电子邮件,它公开NewMail事件。其他类型(比如fax和pager)的对象登记对于该事件的关注。mailManager收到新电子邮件会引发该事件,造成邮件分发给每一个已登记的对象。每个对象都用它们自己的方式处理邮件。
应用程序初始化时只实例化一个mailManager实例,然后可以实例化任意数量的fax和pager对象。
设计要公开事件的类型
开发人员通过连续几个步骤定义公开了一个或多个事件成员的类型。本节详细描述了每个必要的步骤。
第一步:定义类型来容纳所有需要发送给事件通知接受者的附加信息
事件引发时,引发事件的对象可能希望向接收事件通知的对象传递一些附加信息。这些附加信息需要封装到它自己的类中,该类通常包含一组私有字段,以及一些用于公开这些字段的只读属性。根据约定,这种类应该从system.eventArgs派生,而且类名以EventArgs结束。本例将该类命名为newMailEventArgs类,它的各个字段分别表示了发件人、收件人和主题。
/// <summary> /// 第一步:定义类型来容纳所有需要发送给事件接收者的附加信息 /// </summary> internal sealed class NewMailEventArgs : EventArgs { private readonly String m_from, m_to, m_subject; public NewMailEventArgs(String from, String to, String subject) { m_from = from; m_to = to; m_subject = subject; } /// <summary> /// 邮件发件人 /// </summary> public String From { get { return m_from; } } /// <summary> /// 邮件收件人 /// </summary> public String To { get { return m_to; } } /// <summary> /// 邮件主题 /// </summary> public String Subject { get { return m_subject; } } } //后续的步骤在MailManager类中进行 internal class MailManager { }
注意:eventArgs类在Fcl中定义,实现如下
[ComVisible(true),Serializable] public class EventArgs { public static readonly EventArgs Empty =new EventArgs(); public EventArgs(){} }
可以看出,该类型的实现非常简单,就是一个让其他类型继承的基类型。许多事件都没有附加信息需要传递。例如,当一个button向已登记的接受者通知自己被单击时,调用回调方法就可以了。定义不需要传递附加数据的事件时,可直接使用EventArgs.Empty,不用构造新的EventArgs对象。
第二步:定义事件成员
事件成员使用c#关键字event定义。每个事件成员都要指定一下内容:可访问性标识符,委托类型(指出要调用的方法的原型)以及名称(可以是任何有效的标识符)。以下是我们的mailManager类的事件成员
internal class MailManager { // 第二步:定义事件成员 public event EventHandler<NewMailEventArgs> NewMail; ...... }
NewMail是事件名称。事件成员的类型是EventHandler<NewMailEventArgs>,意味着”事件通知”的所有接受者都必须提供一个原型和EventHandler<NewMailEventArgs>委托类型匹配的回调方法。由于泛型system.EventHandler委托类型的定义如下:
public delegate void Eventhandler<TEventArgs>(Object sender, TEventArgs e) where TEventArgs: EventArgs;
所以方法原型必须具有以下形式:
void MethodName(Object sender, TEventArgs e);
第三步:定义负责引发事件的方法来 通知事件的登记对象
根据约定,类应该定义一个受保护的虚方法。引发事件时,当前类及其派生类中的代码会调用该方法。该方法要获取一个参数,也就是一个NewMailEventArgs对象。在这个对象中,包含了传给通知接受对象的信息。该方法的默认实现只检查一下是否有对象登记了对该事件的关注。如果有,就引发事件来通知事件的登记对象。
internal class MailManager { ...... /// <summary> /// 第三步:定义负责引发事件的方法来 通知事件的登记对象 /// 如果类是密封的,这个方法要声明为私有和非虚 /// </summary> /// <param name="e"></param> protected virtual void OnNewMail(NewMailEventArgs e) { // 处于线程安全考虑,现在将对委托字段的引用复制到一个临时字段中 EventHandler<NewMailEventArgs> temp = NewMail; // 任何方法登记了对事件的关注,就通知它们 if (temp != null) { temp(this, e); } } ..... }
以线程安全的方式引发事件
.net framework 刚发布时建议开发者使用以下方式引发事件
protected virtual void OnNewMail(NewMailEventArgs e){ if(NewMail!=null) NewMail(this,e) }
这个方法的问题在于,虽然线程检查检查出NewMail不为null,但就在调用NewMail之前,另一个线程可能从委托链中移除一个委托,使NewMail变成null。解决方法就是定义一个变量去引用NewMail
var temp= NewMail;
为了方便见,可以定义扩展方法来封装这个线程安全逻辑
public static class EventArgExtensions { // where关键字用于约束:TEventArgs 必须是 EventArgs 类型的 public static void Raise<TEventArgs>(this TEventArgs e, object sender, ref EventHandler<TEventArgs> eventDelegate) where TEventArgs : EventArgs { // 出于线程安全的考虑,现在将对委托字段的引用复制到一个临时字段中 EventHandler<TEventArgs> temp = Interlocked.CompareExchange(ref eventDelegate, null, null); // 任何方法登记了对事件的关注,就通知他们 if (temp != null) { temp(sender, e); } } }
现在可以像下面重写onNewMail方法:
protected virtual void OnNewMail(NewMailEventArgs e) { e.Raise(this,ref m_NewMail); }
第四步:定义方法将输入转换为期望事件
你的类还必须有一个方法获取一些输入,并把它转换为事件的引发。在MailManager的例子中,是调用SimulateNewMail方法指出一封新的电子邮件已到达MailManager:
internal class MailManager { ...... // 第四步:定义方法将输入转换为期望事件 public void SimulateNewMail(String from, String to, String subject) { // 构造一个对象来容纳想传给通知接收者的消息 NewMailEventArgs e = new NewMailEventArgs(from, to, subject); // 调用虚方法通知对象事件已发生 OnNewMail(e); } ...... }
编译器如何实现事件
我们仔细扒一扒事件是什么?它是如何工作的。
在MailManager类中,我们用一行代码定义了事件成员:
public event EventHandler<NewMailEventArgs> NewMail;
我们来看看发生了什么?
C#编译器在编译这行代码时,它会把它转换为3个构造:
// 1. 一个被初始化为null的私有委托字段 private EventHandler<NewMailEventArgs> NewMail= null; //2. 一个公共add_Xxx方法(其中Xxx是事件名) //允许方法登记对事件的关注 public void add_NewMail(EventHandler<NewMailEventArgs> value) { //通过循环对CompareExchange的调用 //可以以一种线程安全的方式向事件添加委托 EventHandler<NewMailEventArgs> handler2; EventHandler<NewMailEventArgs> newMail = this.NewMail; do { handler2 = newMail; EventHandler<NewMailEventArgs> handler3 = (EventHandler<NewMailEventArgs>) Delegate.Combine(handler2, value); newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, handler3, handler2); } while (newMail != handler2); } //3. 一个公共remove_Xxx方法(其中Xxx是事件名) //允许方法注销对事件的关注 public void remove_NewMail(EventHandler<NewMailEventArgs> value) { //通过循环对CompareExchange的调用 //可以以一种线程安全的方式向事件移除委托 EventHandler<NewMailEventArgs> handler2; EventHandler<NewMailEventArgs> newMail = this.NewMail; do { handler2 = newMail; EventHandler<NewMailEventArgs> handler3 = (EventHandler<NewMailEventArgs>) Delegate.Remove(handler2, value); newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, handler3, handler2); } while (newMail != handler2); }
第一个构造是具有恰当委托类型的字段。该字段是对一个委托列表的头部的引用。事件发生时会通知这个列表中的委托。字段初始化为null,表示无监听者登记对该事件的关注。
注意,即使原始代码中将事件定义为public,委托字段(本例是NewMail)也始终是private。
第二个构造是一个方法,它允许其他对象登记对该事件的关注。C#编译器在事件名(NewMail)之前附加了add_前缀,从而自动命名该方法。C#编译器还自动为方法生成代码。生成的代码总是调用System.Delegate的静态Combine方法,它将委托事实例添加到委托列表中,返回新的列表头(地址),并将这个地址存回字段。
第三个构造是一个方法,它允许其他对象移除对该事件的关注。C#编译器在事件名(NewMail)之前附加了remove_前缀,从而自动命名该方法。C#编译器还自动为方法生成代码。生成的代码总是调用System.Delegate的静态Remove方法,它将委托事实例从委托列表中移除,返回新的列表头(地址),并将这个地址存回字段。
除了生成上述3个构造,编译器还会在托管程序集的元数据中生成一个事件定义记录项。这个记录项包含了一些标志(flag)和基础委托类型,还引用了add和remove访问器方法。这些信息的作用就是建立"事件"的抽象概念和它的访问器方法之间的联系。编译器和其他工具可利用这些元数据信息,并可通过System.Reflection.EventInfo类获取这些信息。但是,CLR本身并不使用这些元数据信息,它在运行时只需要访问器方法。
设计侦听事件的类型
本节演示如何定义一个类型来使用另一个类型提供的事件。
internal sealed class Fax { /// <summary> /// 将MailManager对象传给构造器 /// </summary> /// <param name="mm"></param> public Fax(MailManager mm) { // 构造EventHandler<NewMailEventArgs>委托的一个实例, // 使它引用我们的FaxMsg回调方法 // 向MailManager的NewMail时间等级我们的回调方法 mm.NewMail += FaxMsg; } /// <summary> /// 新邮件到达时,MailManager将调用这个方法 /// </summary> /// <param name="sender">MailManager对象,便于回调</param> /// <param name="e">NewMailEventArgs对象</param> private void FaxMsg(Object sender, NewMailEventArgs e) { Console.WriteLine("Faxing mail message:"); Console.WriteLine(" From={0}, To={1}, Subject={2}", e.From, e.To, e.Subject); } // 执行这个方法,Fax对象指向NewMail事件注销自己对它的关注,以后便不会接受通知 public void Unregister(MailManager mm) { // 向MailManagerde的NewMail事件注销自己对这个事件的关注 mm.NewMail -= FaxMsg; } }
在Fax构造器中,Fax对象使用C#的+=操作符登记它对MailManager的NewMail事件的关注。
mm.NewMail += FaxMsg;
因为C#编译器内建对事件的支持,所以会将+=操作符翻译成为以下代码来添加对事件的关注:
mm.add_NewMail (new EventHandler<NewMailEventArgs>(this.FaxMsg));
即使使用的编程语言不直接支持事件,也可以显示调用add访问器方法向事件登记一个委托。两者效果是相同的,后者只是源代码看起来没那么优美而已。两者最终都是用add访问器将委托添加到事件的委托列表中,从而完成委托向事件的登记。
在Unregister方法中,我们使用了"-="来向MailManagerde的NewMail事件注销自己对这个事件的关注,当C#编译器看到使用"-="操作符时向事件注销一个委托时,会生成对事件的remove方法的调用:
mm.remove_NewMail (new EventHandler<NewMailEventArgs>(this.FaxMsg));
和"+="一样,即使使用的编程语言不直接支持事件,也可以显示调用remove访问器方法向事件注销一个委托。remove方法为了向事件注销委托,需要扫描委托列表来寻找恰当的委托。如发现一个匹配,该委托会从事件的委托列表中删除。如果没发现匹配,那么不会报错,列表不会发生变化。
C#要求代码使用+=和-=操作符在列表中增删委托。
显式实现事件
System.Windows.Forms.Control类型定义了约70个事件。假如Control类型在实现事件时,是允许编译器隐式生成add和remove访问器方法以及委托字段,那么每个Control对象都会包含70个委托字段。由于大多数开发人员只关心少数几个事件,所以从Control派生类型创建的对象都会浪费大量内存。
目前。讨论C#编译器如何允许类的开发人员显式实现一个事件,使开发人员能够控制add和remove方法来操纵回调委托的方式。这里演示如何通过显式实现事件来高效率地实现一个提供大量事件的类。
为了高效率存储事件委托,公开了事件每个对象都要维护的一个集合(通常是一个字典)。这个集合将某种形式的事件标识作为键(key)。新对象构造时,这个集合是空白的。登记对一个事件的关注时,会在集合中查找事件的标识符。如果事件标识符已在其中,新委托就和这个事件的委托列表合并。如果事件标识符不再集合中,就添加事件标识符和委托。
对象需要引发一个事件时,会在集合中查找事件标识符。如果集合中没有找到事件标识符,表明还没有任何对象登记对这个事件的关注,所以没有任何委托需要回调。如果事件标识符在集合中,就调用它关联的委托列表。这个设计模式的实现是定义了事件那个类型的开发人员的责任;使用类型的开发人员不知道事件在内部是如何实现的。
下面展示了如何完成这个模式的。首先实现的是一个EventSet类,它代表一个集合,其中包含了事件以及每个事件的委托列表。
/// <summary> /// 这个类目的是在使用EventSet时,提供多一点的类型安全性和代码可维护性 /// </summary> public sealed class EventKey : Object { } /// <summary> /// 代表一个集合,其中包括了事件以及事件的委托列表 /// </summary> public sealed class EventSet { /// <summary> /// 私有字典,用于维护EventKey -> Delagate 映射 /// </summary> private readonly Dictionary<EventKey, Delegate> m_events = new Dictionary<EventKey, Delegate>(); /// <summary> /// 添加一个 EventKey -> Delegate 映射 (如果EventKey不存在) /// 或者将一个委托与一个现有的EventKey合并 /// </summary> /// <param name="eventKey"></param> /// <param name="handler"></param> public void Add(EventKey eventKey, Delegate handler) { Monitor.Enter(m_events); Delegate d; m_events.TryGetValue(eventKey, out d); m_events[eventKey] = Delegate.Combine(d, handler); Monitor.Exit(m_events); } /// <summary> /// 从EventKey(如果存在)删除一个委托,并且 /// 再删除最后一个委托时删除EventKey -> Delegate 映射 /// </summary> /// <param name="eventKey"></param> /// <param name="handler"></param> public void Remove(EventKey eventKey, Delegate handler) { Monitor.Enter(m_events); // 调用TryGetValue,确保在尝试从集合中删除一个不存在的EvenrKey时, // 不会抛出异常 Delegate d; if (m_events.TryGetValue(eventKey, out d)) { d = Delegate.Remove(d, handler); // 如果还有委托,就设置新的头部(地址),否则删除EventKey if (d != null) m_events[eventKey] = d; else m_events.Remove(eventKey); } Monitor.Exit(m_events); } /// <summary> /// 为指定的EventKey -> Delegate 映射引发事件 /// </summary> /// <param name="eventKey"></param> /// <param name="sender"></param> /// <param name="e"></param> public void Raise(EventKey eventKey, Object sender, EventArgs e) { // 如果EventKey不在集合中,不抛出异常 Delegate d; Monitor.Enter(m_events); m_events.TryGetValue(eventKey, out d); Monitor.Exit(m_events); if (d != null) { // 由于字典可能包含几个不同的委托类 // 所以无法在编译时构造一个类型安全的委托调用 // 因此,我调用System.Delegate类型的DynamicInvoke方法, // 以一个对象数组的形式向它传递回调方法的参数。 // 在内部,DynamicInvoke会向调用的回调方法查证参数的 // 类型安全性,并调用方法。 // 如果存在类型不匹配情况,则抛出异常 d.DynamicInvoke(new Object[] { sender, e }); } } }
接着,让我们定义一个使用EventSet类。在这个类中,一个字段引用了一个EventSet对象,而且这个类的每一个事件都是显示实现的,使用每个事件的add方法都将制定的回调委托存储到EventSet对象中,而且每个事件的remove方法都删除指定的回调委托。
using System; // 为这个事件定义从EventArgs派生的类型 public class FooEventArgs : EventArgs { } internal class TypeWithLotsOfEvents { // 定义一个虚实例字段,它引用一个集合 // 集合用来管理一组"事件/委托"对 // 注意: EnentSet类型不是FCl一部分,它是我们自己定义的类型 private readonly EventSet m_eventSet = new EventSet(); // protected属性使派生类型能访问集合 protected EventSet EventSet { get { return m_eventSet; } } #region 用于支持Foo事件的代码 (为附加的事件重复这个模式) // 定义Foo事件必要的成员 // 2a. 构造一个静态只读对象来标识这个事件 // 每个对象都有它自己的哈希码,以便在对象的集合中查找这个事件的委托链表 protected static readonly EventKey s_fooEventKey = new EventKey(); // 2d. 定义事件的访问器方法,用于在集合中增删委托 public event EventHandler<FooEventArgs> Foo { add { m_eventSet.Add(s_fooEventKey, value); } remove { m_eventSet.Remove(s_fooEventKey, value); } } // 2e. 为这个事件定义受保护的虚方法OnFoo protected virtual void OnFoo(FooEventArgs e) { m_eventSet.Raise(s_fooEventKey, this, e); } // 2f. 定义将输入转换成为这个事件的方法 public void SimulateFoo() { OnFoo(new FooEventArgs()); } #endregion }
使用TypeWithLotsOfEvents类型的代码不知道事件是由编译器隐式实现的,还是由开发人员显式实现的。它们只需要标准语法向事件登记即可:
public static void Main() { TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents(); // 添加一个回调 twle.Foo += HandleFooEvent; twle.SimulateFoo(); } private static void HandleFooEvent(object sender, FooEventArgs e) { Console.WriteLine("Handling Foo Event here..."); }