c#

位置:IT落伍者 >> c# >> 浏览文章

.NET家族新成员:G#语言简介


发布日期:2019年07月27日
 
.NET家族新成员:G#语言简介

什么是G#

G#是我在过去几个月里构思出来的一种新的程序设计语言其目的是生成类型安全的代码这些代码能够在编译时或运行时被注入(Inject)到一个代码基(Code Base)中其语法是C# 的一个超集和其他代码生成技术与工具(如CodeSmith一种伟大的工具/语言)不同G#并不打算生成用作起始点(Starting Point)或用于消费(Consumption)的代码取而代之G#使用了面向方面的程序设计(AOP)技术来向客户代码中注入代码我们会快速地介绍一下AOP因为它对很多开发者来说还是崭新的

AOP

AOP或称面向方面的软件开发(AOSD)于年在Xerox Parc创建是一种相对先进的软件典范(Paradigm)其思想很简单通过使开发者每次只关注一个问题域来降低软件开发的复杂性换句话说人们在尝试解决一个业务问题(比如在互联网上销售产品)时无需考虑安全线程登录数据访问和其他领域的问题这被称为关注点的分离(Separation of Concerns)通过分离这些领域或者方面某一特殊方面的专家可以开发能够解决该方面问题的最好的解决方案因此开发者无需再去掌握所有的行业这样就有望产生健壮并且功能完善的软件因为开发者只需做一名软件问题域的专家

AOP通过定义方面(也就是一组行为)来开始然后将代码注入到适当的方法中去每个代码注入点都被称作是一个结合点(Join Point)让我们以安全为例所有的输入都是邪恶的是安全界的一条曼特罗(Mantra咒语)对抗这一难题的一种做法是要求所有的开发者编写代码时都要在使用数据之前检查是否有恶意的输入开发者们很可能会开发一个辅助方法用来解决这一问题然后所有的开发者都会在他们的代码中简单地调用这个辅助方法AOP可以解决这一问题它抽取这些相同的辅助方法并创建一个方面然后将其注入到需要对用户输入进行检查的地方这个过程称为编排(Weaving)我们没有简单地定义一个将会收到邪恶输入方面的位置列表而是定义了将要使用的一组标准(Criteria)既然是这样我们就希望除方面之外能够注入所有带有参数的公共属性方法和构造器比起创建一个列表这样做的好处是开发者们无需再凭借他们的记忆来将需要对输入进行检查的方法添加到列表中

相对于你所熟悉的AOP语言如ASPectJG#并没有单独的编排文件编排被集成到了语法当中对于大多数程序员来说别人可以将代码注入到他们的代码基之中这无疑是一种容易引起恐慌的建议为了解决这一问题G#包含了一个用来处理这一问题的安全模型并且允许程序员来控制哪些人可以注入代码以及可以注入什么样的代码这将放在后面进行讨论在我们深入之前先来看一些基础要素

基础

public class Client

{

public Client()

{

Messenger(Hello World);

}

private void Messenger(string message)

{

ConsoleWriteLine(message);

}

}

public generator Rename

{

static generation ChangeIt : target ClientMessenger(string message)

{

pre

{

string oldMessage = message;

message = Hello G#;

}

post

{

message = oldMessage;

}

}

}

尽管这个例子没有任何用途但它演示了G#的大量特性首先Client类使用了标准的C#语法——这在G#中是有效的它只是简单地向控制台输出了消息Hello World这个类定义下面是G#中新增的语言构造称作生成器(Generator)现在只需认为生成器是所有用于定义如何生成代码的代码的容器即可这和类(Class)类似Rename是这个生成器的名字就好像Client是类的名字一样接下来定义了一个名为ChangeIt的生成(Generation)生成和方法类似每次调用它都会执行一些动作不同的是在调用生成的时候会通常产生代码注意ChangeIt有一个目标(Target)在这里是来自Client类的Messenger方法目标可以是任何(语言)构造并且还可以包括通配符和正则表达式来指定一组项目作为目标这表示由该生成所发出(Emit)的所有代码都将被注入到Messenger方法中关键字pre规定了其后面花括号中定义的所有代码都将被注入到Messenger方法体中定义的代码之前关键字post规定了其后面花括号中定义的所有代码都将被注入到Messenger方法体中定义的代码之后因为用关键字static标记了这个生成因此代码的实际注入是编译过程的一部分理解这一点很重要程序员将无法看到Messenger方法的变化除非使用ildasm或Reflector来检查Messenger方法此外还有一个目前还只是梦想的特性就是能够生成动态的Region这样在Visual 中就能打开它来检查生成器都在客户环境中生成了哪些代码稍后我们将讨论其他类型的生成

private void Messenger(string message)

{

// From ChangeIt pre block

string oldMessage = message;

// From ChangeIt pre block

message = Hello G#;

// From the Messenger method body

ConsoleWriteLine(message);

// From ChangIt post block

message = oldMessage;

}

这个方法因此将向控制台打印Hello G#然后再将message字符串改回最初传入的消息注意在NET中字符串是不可变的因此实际上是不能改变一个字符串所包含的内容的因此通过在post块中将message改回初始的消息以保护Messenger方法外的Hello World消息并不是必须的但是对于在Messenger方法体中执行的任何代码来说后置的注入代码都是很重要的这里出现的一个逻辑问题是在后置条件(Post Condition)之后Messenger方法体中的代码究竟什么时候执行呢?这个问题完美地引出了下一节

生成器的继承

我们上面的例子表明生成器就是生成的包容器但是其中还可以包含类能够包含的所有成员(如方法属性事件等等)此外可见性和其他修饰符如virtual也可以用于生成因此生成器是面向对象的并且可以彼此继承这样做的原因和类类似这允许基生成器定义一个基本的注入行为并由子生成器定义更多的特殊的行为

public class Client

{

protected string message;

public Client()

{

ssage = Hello World;

Messenger(ssage);

}

private void Messenger(string message)

{

onsoleWriteLine(message);

}

}

public generator Base

{

protected virtual generation ChangeIt : target ClientMessenger(*)

{

pre

{

string message = Hello G#;

}

post

{

ssage = message;

}

}

}

public generator Sub : Base

{

protected override generation ChangeIt : target ClientMessenger(string message)

{

pre

{

basepre();

message = ssage;

}

post

{

ssage = message;

basePost();

}

}

}

下面给出了发出的Messenger方法我们来分解一下这些代码Sub生成器从Base生成器派生而来并且重写了基类中的方法ChangeIt基类中使用星号(*)定义了一个目标它可以被任何参数取代这意味着它的目标可以是Client类中Messenger的所有重载形式稍后我们将介绍定义目标的细节凭经验就可以知道一个基本的规则是在重写的生成中必须为目标指定更多的特性在代码的另外一部分中我们使用了关键字base来访问基生成器的pre和post因此我们可以决定是在Base生成器发出代码之前还是之后发出Sub生成器的代码

private void Messenger(string message)

{

// Base

string ssage = Hello G#;

// Sub

message = ssage;

ConsoleWriteLine(message);

// Sub

ssage = message;

// Base

ssage = ssage;

}

捕获

关键字capture用于引用在同一个生成的作用域中定义的变量即使这个变量定义在基生成器中能够访问这些变量的原因是所有生成的代码都将位于相同的作用域中在访问被捕获(Capture)的变量时关键字capture并不是必需的但这里的Messenger方法使用了同名的变量在这种情况下就需要关键字capture来解决混淆问题变量message定义在Base生成器的ChangeIt生成中而其目标Messenger方法中也有可能定义同名的参数因为我们在定义中使用了星号(*)通配符这种请况很可能发生因为生成中可以定义局部变量并且稍后在其目标方法的重载中也可以定义同名的局部变量如果G#不对其采取行动的话当目标方法中定义了和生成中的局部变量同名的变量时就会引发一个编译错误

分节符

为了指出如何发出代码G#提供了能够通过执行代码来取代发出代码这通过§符号来实现该符号称作分节符(Section Sign)该符号在Times New Roman字体中是这样的§而在Courier New字体(译注原文是Courier字体这里为了同一代码格式使用了Courier New字体两者非常相似)中是这样的§当在代码中放置了§的时候其后的代码将被执行而不是被发出

pre

{

§ for(int i = ; i < ; i++)

§ {

ConsoleWriteLine(i);

§ }

}

绿色高亮的代码在编译期间将被执行而不是被发出从这个pre块发出的代码是这样的

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

ConsoleWriteLine();

注意当这几行代码被发出时i被它的整数值取代了G#知道如何注入基本类型如int和float的值但他无法发出类或其他自定义的复杂类型如果§后跟了一个方法该方法的返回值类型必须是基本类型void或emit如果是其他类型则编译过程将会破坏返回的所有东西我们将在下一节里解释关键字emit我从来没有见过哪个键盘上有§符号不过可以通过定义组合快捷键来产生这个符号我选择Ctrl+l(小写的L)来在Word里输出这个符号并且在Visual 中为这个快捷键组合写了一个宏来输出这个符号

关键字emit

我们已经讨论了如何使用关键字pre和post来发出代码但G#中有更丰富的方法来指定如何以及在哪里发出代码其中一种方法就是像使用pre和post那样使用关键字emit

emit

{

ConsoleWriteLine(Hello G#);

}

代码ConsoleWriteLine(Hello G#);会在哪里发出?它将在其基生成的emit块中发出[(That reminds be of the definition of a normal)]OK那么pre和post实际上也是emit块只不过它们定义了发出代码的位置(方法体的前面和方法体的后面)对于上面的代码片断我们需要提供一个上下文环境来说明一下这些代码是在哪里发出的

pre

{

§ Counter();

}

void Counter()

{

emit

{

ConsoleWriteLine(The emit keyword in action);

}

}

当一个带有该pre块的生成被编译时它会调用Counter方法因为Counter()的前面有§符号在Counter方法中关键字emit用于注入对ConsoleWriteLine的调用emit块将会用块中的代码来取代对Counter()的调用一个方法中emit块的数量没有任何限制并且可以在emit块中使用§

此外emit只是对G#框架(G# Framework)中定义的Emit类型的一个映射因此我们可以创建emit的实例

pre

{

§ DisplayParts();

}

public emit DisplayParts()

{

emit partOne partTwo;

partOne

{

§ Injector(partTwo);

ConsoleWriteLine(Part One);

§ partTwoEmit();

}

return partOneEmit();

}

private void Injector(emit target)

{

target

{

ConsoleWriteLine(Injection);

}

}

在上面的代码片断中我们在DisplayParts生成的定义中创建了两个emit对象partOne和partTwo然后我们使用partOne加花括号定义了一个emit块花括号之间的所有代码都将被发出到partOne的局部存储(Local Store)中当我们在partOne对象上调用Emit方法时将会返回这个局部存储最后注意该代码段的pre块中调用了返回值类型为emt的DisplayParts[Since the emitted code is not caught it is emitted into the pre block]

目标

我们已经探讨了当以一个方法为目标时如何使用关键字pre和post但除此之外G#还定义了一些关键字以使用其他语言构造作为目标下面的表格给出了其他能够发出代码的关键字和它们的描述为这些关键字指定目标构造时也可以使用通配符参见后面的示例

关键字 描述

class 注入目标命名空间中所有的类

namespace 注入目标命名空间中所有的命名空间

set | get 注入目标所定义的所有set和get区域

generator 注入目标所定义的所有生成器

generation 注入目标所定义的所有生成

property 注入目标所定义的所有属性

method 注入目标所定义的所有方法

public generator Base

{

protected virtual generation ChangeClient : target Client

{

property public string *

{

get

{

post

{

ConsoleWriteLine(value);

}

}

set

{

pre

{

ConsoleWriteLine(value);

}

}

}

method (public | protected) * Cl*(*)

{

ConsoleWriteLine(Cl* Method Targeted);

}

}

}

这里我们注入了所有类型为string而名字任意的属性我们还在get访问器中使用了关键字value该关键字在G#中表示由目标代码的get访问器所返回的值在这里使用pre和post与在方法中的用法无异接下来的关键字method定义了我们将要注入的所有公共的和受保护的方法其中两个星号(*)分别表示返回值类型任意并且方法的名字是以Cl开头后跟任意多个任意的字符(译注实际上是个星号后面括号里那个表示该方法能够带任意多的参数)在名字中还可以使用英镑($)符号作为通配符表示任意的一个字符注意到这一点很重要Client类中所有满足约束条件的成员都会被注入

自适应生成

第二种生成的类型是自适应生成(Adaptive Generation)只是简单地把一个生成前面的关键字static换成adaptive自适应生成在运行时生成并且注入代码因此它可以检查对象的状态以指导生成

比起静态生成自适应生成的优势在于第三方也可以提供生成框架和组件第三方开发者可以通过创建幻象目标(Phantom Target)来以他们一无所知的代码基作为目标幻象目标并不存在于生成框架或目标框架中当开发者希望使用一个第三方的生成器时他们可以加入幻象的命名空间方法并将生成的代码重定位到他们的代码基中适当的位置

public class Client

{

protected string message;

public Client()

{

ssage = Hello World;

Messenger(ssage);

}

public string Message

{

get

{

return ssage;

}

}

private void Messenger(string message)

{

ConsoleWriteLine(message);

}

}

// Phantom Target

namespace ThirdPartySecurity

{

public adaptive generator Input : target Client

{}

}

程序集

// Third Party generator

public generator Security

{

protected adaptive generation CheckInput

: target ThirdPartySecurityInput

{

property public string *

{

get

{

pre

{

value = ValidateInput(value);

}

}

}

method public * *(all string *(input))

{

pre

{

input = ValidateInput(input);

}

}

}

}

在上面的代码中我们定义了一个Client类一个第三方生成器Security和一个幻象目标命名空间ThirdPartySecurity类和幻象目标被定义在一个程序集中而第三方生成器在另外一个程序集中提供第三方定义了所有类型为string的公共属性在返回之前都要调用ValidateInput方法它还定义了所有返回值类型为string的公共方法在执行任何代码前都要对其类型为string的参数调用ValidateInputG#中的关键字all表示对于作用域内所有符合标准的参数都要做这件事情星号(*)表示参数的名字可以是任意的我们必须将想要引用的实参的名字放在圆括号中以告诉编译器我们正在使用这个名字但我们不希望将它作为标准的一部分

现在的CLR能够在运行时动态地注入IL代码这发生在程序集加载时通过Profiler API完成然而这种途径还存在着一系列的安全问题因为它禁用了CAS因此还需要深入的研究才能找到一种切实可行的解决方案我们将在下面描述这是如何完成的 CAS和注入特性

现在已经有望解决注入代码所引发的安全问题了G#的安全模型能够确保只有你希望他注入代码的人才能注入代码并且这些代码只能限制在你所允许的代码访问安全(CASCode Access Security)许可中通过使用元数据你可以声明你授予注入代码的权限这仍需要定义一种语法并加入建议[Still need to define this syntax and open to suggestions]所有包含生成器和生成的程序集都必须被赋予一个强密钥然后为目标程序集添加一个带有该公共密钥记号的Injector特性只有在Injector中指出了强密钥的程序集才能运行和注入代码

总结

代码生成为我们提供了各种可能性我们希望G#能够发展成为一个泛型的类型安全的代码生成语言根据您的意见和建议G#的语法还会改变并且进一步精炼因此非常感谢您阅读G#的相关文档如果您有任何意见问题或想法请给Ernie Booth发emailgsharp@ernieboothname

               

上一篇:Visual Basic 10:持续改进中

下一篇:Castle在“新.NET时代”将何去何从