元编程(Metaprogramming)是编写、操纵程序的程序,简而言之即为用代码生成代码。元编程是一种编程范式,在传统的编程范式中,程序运行是动态的,但程序本身是静态的。在元编程中,两者都是动态的[1]。元编程将程序作为数据来对待,从而赋予了编程语言更加强大的表达能力。

编写元程序的语言称之为元语言,被操纵的语言称之为目标语言[2]。根据元语言和目标语言是否相同,我们可以将元编程分为两类:

当元语言即目标语言本身时,元编程是目标语言所支持的高级特性,是在编译期或运行期生成或改变代码的一种编程形式,是狭义上的元编程;当元语言并非目标语言时,元编程侧重代码内容的生成,并不关注目标语言代码的编译和执行,也可以称之为产生式编程(Generative Programming)或者代码生成技术(Code Generation)。我们按照从易到难的顺序来依次介绍这些技术。

元语言非目标语言

比较低阶的方式是用直接用处理文本的方式生成代码,其次是用IDE的可视化特性、以及用模版引擎的方式,而最高级的方式应该是用编译原理的方式实现。

1. 文本处理

几乎所有的编程语言都有输入输出文本的能力。利用文本输出能力生成具体代码是最简单的元编程手段。其实用这种方式可以生成任何一种语言的代码,之所以把它归类于"元语言非目标语言",因为它对目标语言的代码仅仅当作一种文本来处理。来看一个bash脚本的示例:

#!/bin/sh

# metaprogram

echo '#!/bin/sh' > program

for i in $(seq 992)

do

echo "echo $i" >> program

done

chmod +x program

这个脚本没有任何输入,生成了一个新的993行的脚本来打印输出数字1至992。这并不是打印一串数字最有效的方法。尽管如此,程序员可以在几分钟内编写和执行这个元程序,生成了近1000行的代码,简单粗暴。

#!/bin/sh

echo 1

echo 2

echo 3

...

echo 992

2. IDE特性

通过可视化IDE生成代码的编程探索可谓历史悠久,最早开始的是桌面端IDE,进入Web时代后诞生了富文本编辑器,随后又产生了一些脚手架框架。在页面上拖拖拽拽、快捷的操作命令就能生成代码,能够大大提升构建工程的速度。

VB 6.0的操作界面 - 图片来自于 Visual Basic

对于这种元编程方式而言,大都针对特定的IDE,大部分情况下我们只是普通用户,除了IDE的设计者很少有人去了解其背后的实现机制。当然有些IDE也会提供插件定制功能,这时候便有机会在其基础上进行元编程开发。

Eclipse上的Mybatis配置文件生成插件 - 图片截图于Eclipse Marketplace

3. 模板引擎

几乎所有的Web后端语言都有生成HTML的模版引擎技术(Template engines),通过变量替换、表达式处理等方式来简化前端页面编写逻辑,更好地实现用户界面与业务数据的分离,提高前端代码的可维护性。

虽然现在前后端分离已经大行其道,大部分情况下后端程序员无需关心前端页面的实现,但是当后端逻辑里涉及到HTML、XML和其他格式化文本的生成时,模板引擎依然是我们的最佳备选方案。

不论是Java的FreeMarker/Velocity/Thymeleaf,JS的Pug,还是Python的Jinja/Tornado,上手都很简单。jinja2号称解析速度快被广泛使用,以它来做个示范:

模板文件

My Webpage

{% for item in navigation %}

{{ item.caption }}

{% endfor %}

My Webpage

{{ a_variable }}

加载模板并传入变量

>>> from jinja2 import Environment, FileSystemLoader

>>> env = Environment(loader=FileSystemLoader('/path/to/templates'))

>>> template = env.get_template('mytemplate.html')

>>> print(template.render(navigation=[{'href':'foo.com','caption':'bar'}], a_variable='Hello World'))

解析结果

My Webpage

  • bar

My Webpage

Hello World

4. 编译原理

同样是处理结构化文本,基于词法分析和语法分析而实现的元编程比模板引擎更为强大灵活,要知道,编译器就是最常见的元编程形式(将源代码编译为机器指令)。基于Lex、Yacc和ANTLR之类的编译工具,即便我们不精通编译原理,也能完成语法的解析和代码的生成,甚至创造一门自己的编程语言。

语法解析基本流程- 图片来自于ANTLR 4权威指南

Lex & YACC

Lex和YACC是UNIX环境下的编译工具,其中Lex是一个词法解析器(Lexical analyzers)生成工具,而YACC是一个语法解析器(Parser)生成工具,两者搭配使用可完成语法解析,生成C/C++代码。

例如,我们想用一门简单的语言去控制一个温度调节器[7][8],例如:

heat on // => 输出Heat turned on or off!

heat off // => 输出Heat turned on or off!

target temperature 22 // => 输出Temperature set!

我们需要辨别的符号有:heat,on/off(STATE),target,temperature,NUMBER。对应的Lex文件如下:

%{

#include

#include "y.tab.h"

%}

%%

[0−9]+ return NUMBER;

heat return TOKHEAT;

on|off return STATE;

target return TOKTARGET;

temperature return TOKTEMPERATURE;

\n /* ignore end of line */;

[ \t]+ /* ignore whitespace */;

%%

这里将符号进行了转换,转换后的符号的定义在头文件y.tab.h中,该头文件则由YACC从语法文件中生成:

/*省略开头部分*/

%token NUMBER TOKHEAT STATE TOKTARET TOKTEMPERATURE

commands: /* empty */

| commands command

;

command:

heat_switch

|

target_set

;

heat_switch:

TOKHEAT STATE

{

printf("\tHeat turned on or off\n");

}

;

target_set:

TOKTARGET TOKTEMPERATURE NUMBER

{

printf("\tTemperature set\n");

}

;

语法树中支持heat_switch和target_set两种命令,每种命令都定义了需要执行的操作。Lex和YACC的代码编译后会生成一个可执行文件:

$ ./example4

heat on

Heat turned on or off

heat off

Heat turned on or off

target temperature 10

Temperature set

target humidity 20

error: parse error

ANTLR

ANTLR 是一个强大的语法分析器生成工具,可以使来读取、处理、执行或翻译结构化文本和二进制文件。它相当于Lex和YACC的组合,而且能支持更广泛的编程语言。它被应用于学术领域和工业生产实践,是众多语言、工具和框架的基石。大名鼎鼎的ORM框架Hibernate便是使用ANTLR来处理HQL语言。

用ANTLR解析语法树示例 - 图片截图自官网

ANTLR的上手流程与Lex&YACC类似。在ANTLR:在浏览器中玩语法解析这篇博文中,作者展示了如何利用ANTLR来快速完成代码的解析和执行,简单易懂,有兴趣的朋友可以进一步了解。

利用ANTLR将目标语言解析并执行 - 图片来自原文

元语言即目标语言

现代的编程语言大都会为我们提供不同的元编程能力。静态元编程主要有宏和泛型,允许程序在编译期展开生成或者执行代码。动态元编程主要靠反射机制,允许程序在运行时改变自身的行为。

5. 静态元编程

宏一个将输入的字符串映射成其他字符串的过程,这个映射的过程也被称作宏展开。很多编程语言,尤其是编译型语言都实现了宏这个特性,然而这些语言却使用了不同的方式来实现宏:一种是基于文本替换的宏,另一种是基于语法的宏[3]。

C语言中的文本替换宏,只是一个简单的标识符,它们会在预编译的阶段被预编译器替换成宏定义中后半部分的字符,类似于变量声明:

#define BUFFER_SIZE 1024

char *foo = (char *)malloc(BUFFER_SIZE); // BUFFER_SIZE => 1024

C语言中同样有简单形式的语法宏,通过在宏的定义中引入参数,宏定义的内部就可以直接使用对应的标识符引入外界传入的参数,同样也是在预编译阶段完成替换,类似于函数定义:

#define plus(a, b) a + b

#define multiply(a, b) a * b

int main(int argc, const char * argv[]) {

printf("%d", plus(1, 2)); // plus(1, 2) => 1 + 2

printf("%d", multiply(3, 2)); // multiply(3, 2) => 3 * 2

return 0;

}

上面两个例子都来自于《谈元编程与表达能力》这篇博文,文中还介绍了Elixir和Rust这两种语言中更高阶的语法宏,不仅卫生宏问题得到了解决,还可以直接使用宏操作上下文的语法树,甚至进行模式匹配、递归解析,的确能够刷新我们对宏的认识。

泛型

泛型(generics)同样是一种编程范式,它允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Java、C#、F#和Swift等语言称之为泛型;Scala 和 Haskell 称之为参数多态;C++ 和D称之为模板[5]。

只有在编辑期能够展开代码的泛型,才能算符合元编程的范畴。以C++ 的模板为例,下面的这个计算阶乘的案例中,factorial<5>::value的值是在编译时而非运行时计算出来的。换句话说,这段代码以模板形式通过编译器生成了新的代码,并在编译期间获得执行[1]。

template

struct factorial

{

enum { value = N * factorial::value };

};

template <> // 特化(specialization)

struct factorial<0> // 递归中止

{

enum { value = 1 };

};

void main()

{

// 以下等价于 cout << 120 << endl;

cout << factorial<5>::value << endl;

}

基于Java的“伪泛型”则无法进行元编程。它是在编译期进行了类型擦除(Type erasure),生成的字节码不包含任何的泛型信息,类型变量被其限定类型(无限定的变量用Object)替换,然后在运行时识别变量的具体类型,所以Java 的泛型要靠编译期和运行期协作实现。

public class MaximumTest

{

// 比较三个值并返回最大值

public static > T maximum(T x, T y, T z)

{

T max = x; // 假设x是初始最大值

if ( y.compareTo( max ) > 0 ){

max = y; //y 更大

}

if ( z.compareTo( max ) > 0 ){

max = z; // 现在 z 更大

}

return max; // 返回最大对象

}

public static void main( String args[] )

{

// 输出结果:3, 4 和 5 中最大的数为 5

System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n\n",

3, 4, 5, maximum( 3, 4, 5 ) );

}

在这个案例中,T会被替换为Comparable。而在maximum( 3, 4, 5 )中变量的实际类型是Integer。

6. 动态元编程

反射

反射是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为[6]。

一般来说,程序中代码的执行逻辑是明确的,运行时引擎(Runtime engine)将代码解析为机器指令,然后计算机按照顺序执行,在此过程中代码无法自己改变执行的顺序,但反射机制允许在运行过程中通过调用运行时引擎暴露的API来实时获取和改变代码,从而可以改变源代码中预设的执行顺序。

反射的实现因语言而异,这也就让不同的语言有不同的元编程体验。

Javascript中的eval函数,将字符串解析为代码并执行:

// 等同于new Foo().bar()

eval('new Foo().bar()')

Java中利用Class和Method类,在运行时加载一个编译时未引用的类,并执行其方法:

try{

Object foo = Class.forName("com.package.Foo").newInstance();

Method m = foo.getClass().getDeclaredMethod("bar");

m.invoke(foo);

} catch(Exception e){

// Catching Exception

}

Objective-C中利用class_addMethod动态的为当前的类添加新的方法和对应的实现[3]:

void dynamicMethodIMP(id self, SEL _cmd) { }

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {

if (aSEL == @selector(resolveThisMethodDynamically)) {

class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");

return YES;

}

return [super resolveInstanceMethod:aSel];

}

Ruby中利用define_singleton_method动态的为当前的类添加新的方法和对应的实现[3]:

class Dog

def method_missing(m, *args, &block)

if m.to_s.start_with? 'find'

define_singleton_method(m) do |*args|

puts "#{m}, #{args}"

end

send(m, *args, &block)

else

super

end

end

end

反射是比较高级的元编程方式,可以用来简化日志处理、异常处理和权限管理等重复的逻辑,提高编程的效率。

引用

java元编程_一文读懂元编程相关推荐

  1. java 审批流_一文读懂工作流

    网上关于工作流引擎有比较多的简介,也有很多工作流的实际应用场景.本文结合笔者多年对工作流的经验来阐述一下对工作流的理解. 一.什么是工作流? 先贴上wiki百科对于工作流的定义 工作流(Workflo ...

  2. java arraylist排序_一文读懂Java集合框架

    欢迎关注微信公众号:深入浅出Java源码 概念 Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们.集合框架被设计成要满足以下几个目标. 该框架必须是高性能的.基本集合(动态数组,链表, ...

  3. java 委派关系_一文读懂java类加载之双亲委派机制

    一个编译后的class文件,想要在JVM中运行,就需要先加载到JVM中.java中将类的加载工具抽象为类加载器,而通过加载工具加载类文件的具体方式被称为双亲委派机制. 知识点 类加载器:通过一个类全限 ...

  4. java中date类型如何赋值_一文读懂java中的Reference和引用类型

    简介 java中有值类型也有引用类型,引用类型一般是针对于java中对象来说的,今天介绍一下java中的引用类型.java为引用类型专门定义了一个类叫做Reference.Reference是跟jav ...

  5. psm倾向得分匹配法举例_一文读懂倾向得分匹配法(PSM)举例及stata实现(一)

    原标题:一文读懂倾向得分匹配法(PSM)举例及stata实现(一) 一.倾向匹配得分应用之培训对工资的效应 政策背景:国家支持工作示范项目( National Supported Work,NSW ) ...

  6. python读取枚举_一文读懂Python 枚举

    enum是一组绑定到唯一常数值的符号名称,并且具备可迭代性和可比较性的特性.我们可以使用 enum 创建具有良好定义的标识符,而不是直接使用魔法字符串或整数,也便于开发工程师的代码维护. 创建枚举 我 ...

  7. python输入什么就输出什么_一文读懂Python的输入和输出

    本文介绍了Python的输入和输出,既然是Python代码,那么就一定有输出量,那么,Python是如何输出的呢? 输出 用print()在括号中加上字符串,就可以向屏幕上输出指定的文字.比如输出'h ...

  8. gps导航原理与应用_一文读懂角速度传感器(陀螺仪)的应用场景

    前文我们大致了解陀螺仪的来历,原理和种类,那么,它与我们的日常生活有怎样的关系呢? 陀螺仪器最早是用于航海导航,但随着科学技术的发展,它在航空和航天事业中也得到广泛的应用.陀螺仪器不仅可以作为指示仪表 ...

  9. hdfs读写流程_一文读懂HDFS分布式存储框架分析

    一文读懂HDFS分布式存储框架分析 HDFS是一套基于区块链技术的个人的数据存储系统,利用无处不在的私人PC存储空间及便捷的网络为个人提供数据加密存储服务,将闲置的存储空间利用起来,服务于正处于爆发期 ...

最新文章

  1. 蓝牙(BLE)应用框架接口设计和应用开发——以TI CC2541为例
  2. 阿里云异构计算团队亮相英伟达2018 GTC大会
  3. java扫描包内所有类_第20天|Java入门有野,修饰符
  4. The method setCharacterEncoding(String) is undefined for the type HttpServletResponse 是什么原因?...
  5. Linux之特殊权限
  6. spring学习笔记二(基于注解)
  7. cuda10安装_Mmdetection的安装和使用
  8. android使用遥控器模拟鼠标拖拽操作
  9. python机器学习之特征值处理(sklearn)
  10. 2021 年 35+ 最佳免费 WordPress 博客主题
  11. ARM嵌入式系统的问题分析与总结
  12. 计算机组成原理是答案,计算机组成原理(上)_答案mooc
  13. 【电子通识】为什么IC需要自己的去耦电容?
  14. flink+mysql+connector_Flink SQL中connector的定义和实现
  15. 解决Uncaught TypeError Cannot read properties of undefined (reading ‘props‘)
  16. SQL优化不会?推荐4 款工具
  17. matlab求26个字母的组合方式,26个字母识别 用matlab实现的
  18. 改变程序员的十大电影与科普视频
  19. ThinkPHP 5.0.23 远程代码执行 漏洞复现
  20. failed to parse the connection string near ‘;serverTimezone=Hongkongamp;characterEncoding=utf-8amp

热门文章

  1. log4j2内容详解
  2. 成功解决raise AssertionError(“Torch not compiled with CUDA enabled“)AssertionError: Torch not compiled
  3. 液体点滴速度监控报警装置(51单片机)
  4. cpu低端计算机配置清单,i3 4160/GTX750Ti剑灵/英雄联盟中低端组装机配置清单
  5. 【水果大全】快看,你属于哪种水果身材?
  6. 华硕服务器主板型号命名规则,装机指南 华硕主板新命名规则解读
  7. Python实现飞机大战(搞怪)游戏!这是你没见过的全新版本!
  8. Docker-Dockerfile学习
  9. deadine怎么修改服务器,PDG使用Deadline配置教程
  10. 并注册烧写钩子 获取启动介质类型_一种基于USB烧写的数据传输方法与流程