第12章 元编程与注解、反射

反射(Reflection)是在运行时获取类的函数(方法)、属性、父类、接口、注解元数据、泛型信息等类的内部信息的机制。这些信息我们称之为 RTTI(Run-Time Type Information,运行时类型信息) 。

注解(Annotation)是我们给代码添加的元数据。使用注解可以写出更加简洁干净的代码,同时还可以在编译期进行类型检查。Kotlin 的注解完全兼容 Java 的注解。

本章介绍 Kotlin 中的注解与反射编程的相关内容。

12.1 元编程简介

说到元编程(Meta-programming),我们从 Meta- 这个前缀开始说起。Meta- 这个前缀在在西方哲学界指的是:关于事物自身的事物。比如,心理学领域有一门专门研究关于人类认知心理的学科叫认知心理学(cognitive psychology)。而还有一门学科是研究人对自己的认知过程的认知,叫做元认知心理学(Meta cognitive psychology ),又称反省认知、监控认知、超认知、反审认知等。元认知的本质是人类对自身认知活动的自我意识和自我调节。

再例如, meta-knowledge 就是“关于知识本身的知识”,meta-data 就是“关于数据的数据”,meta-language 就是“关于语言的语言”,而 meta-programming 也就是“关于编程的编程”, 也就是我们通常所说的“元编程”。

元编程(Meta-programming)是指用代码在编译期或运行期生成或改变代码的一种编程形式。编写元程序的语言称之为元语言,被操纵的语言称之为目标语言。如果一门语言中具备同时是元语言也是目标语言的能力,这就是反射。

一般代码的操作对象是数据,元编程操作的对象是其他代码。无关业务逻辑,只跟当前代码结构相关的代码。比如在Java中在运行时通过反射把所有以*ServiceImpl 结尾的类找出来,加上log日志或者进行监控统计等其它动作。

除非程序的运行期的输入数据会被直接或间接转化成代码,否则元编程不会给程序带来新的逻辑。元编程本质上是一种对源代码本身进行高层次抽象的编码技术。"元编程"比"我们手写代码"多提供了一个抽象层次! 我们其实就是用代码中的元数据(按照一定的协议规则来定义,也就是注解的语法规范)来进行动态插入新代码逻辑,也就是用来动态生成代码的程序。其实,根本没有什么“元编程”,有的只是“编程”。

反射是促进元编程的一种很有价值的语言特性。编程的语言中的泛型支持也使用元编程能力。元编程通常有两种方式:一种是通过应用程序接口(API)来暴露运行时系统的内部信息;另一种方法是在运行时动态执行包含编程命令的字符串。因此,“程序能编写程序”。虽然两种方法都能用,但大多数方法主要靠其中一种。

注解是把编程中的元数据信息直接写在源代码中,而不是保存在外部文件中。

在使用注解之前(甚至在使用之后),XML配置文件被广泛的应用于编程过程中的元数据的描述。后来程序员们逐渐发现XML的维护越来越糟糕了,进而希望直接使用一些和代码紧耦合的“元数据”,而不是像 XML 那样和代码分离。把注解使用的淋漓尽致的 Spring Boot 框架中,基本不需要一行XML配置,几乎全部使用注解就搞定一个 Spring 企业级应用的开发。

“XML vs. Annotation”,这其实是一个 “阴阳交融” 的编程之道,很多时候要看具体的问题场景来决定采用哪种方式。XML配置就是为了分离代码和配置而引入的,而注解是为了希望使用一些和代码紧耦合的东西。万事万物就是这样的阴阳交合辩证发展的过程。

注解是将元数据附加到代码的方法。而反射可以在运行时把代码中的注解元数据获取到,并在目标代码执行之前进行动态代理,实现业务逻辑的动态注入,这其实就是 AOP (Aspect Oriented Programming,面向切面编程(也叫面向方面)的核心思想——通过运行期动态代理(和预编译方式)实现在不修改源代码的情况下, 给程序动态添加新功能的一种技术。

例如,在 Spring 、 Mybatis 、JPA 等诸多框架中的核心功能都是使用了注解与反射的技术来实现的。例如我们常用的 Spring 框架中的各种注解 @Repository 、@Service 、 @Transactional 、@RequestMapping 、@ResponseBody 等),Mybatis 框架中的各种注解 @Select 、 @Update 、@Param 等。

另外,需要重点提到的就是当下非常流行的 Spring Boot 框架。在使用 Spring Boot 开发企业级应用时,完全不需要使用一行 XML 配置,整个的源代码工程都能基于注解来开发(application.propertis配置文件另当别论,更多关于SpringBoot框架开发的知识,我们将在后面的章节中介绍)。

12.2 注解

Kotlin的注解跟Java注解也完全兼容。我们可以在Kotlin代码中很自然地使用Java中的注解。也就是说,我们使用Kotlin语言集成SpringBoot框架开发的过程将会非常自然,几乎跟使用原生Java语言开发一样流畅,同时还能享受Kotlin语言带来的诸多简洁优雅同时还非常强大的特性。

12.2.1 声明注解

Kotlin中声明注解使用 annotation class 关键字。例如,我们声明两个注解Run和TestCase 如下

@Target(AnnotationTarget.CLASS,AnnotationTarget.FUNCTION,AnnotationTarget.VALUE_PARAMETER,AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.RUNTIME)
@Repeatable
@MustBeDocumented
annotation class TestCase(val id: String)@Target(AnnotationTarget.CLASS,AnnotationTarget.FUNCTION,AnnotationTarget.VALUE_PARAMETER,AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.RUNTIME)
@Repeatable
@MustBeDocumented
annotation class Run

从这个关键字上我们就可以看出注解也是一种 class ,编译器同样可以对注解类型在编译期进行类型检查。

我们自定义的注解上面使用的注解,我们称之为元注解(meta-annotation)。我们通过向注解类添加元注解的方法来指定其他属性。元注解说明如下表

元注解名称 功能说明
@Target 指定这个注解可被用于哪些元素 ( 这些元素定义在kotlin.annotation.AnnotationTarget 枚举类中。它们是:类 CLASS, 注解类 ANNOTATION_CLASS,泛型参数 TYPE_PARAMETER,函数 FUNCTION, 属性 PROPERTY, 用于描述域成员变量的 FIELD,局部变量 LOCAL_VARIABLE,VALUE_PARAMETER,CONSTRUCTOR,PROPERTY_GETTER,PROPERTY_SETTER, 用于描述类、接口(包括注解类型) 或enum声明的 TYPE, 表达式 EXPRESSION,文件 FILE,类型别名TYPEALIAS等。
@Retention 指定这个注解的信息是否被保存到编译后的 class 文件中, 以及在运行时是否可以通过反射访问到它, 可取的枚举值有3个,分别是: SOURCE (注解数据不存储在二进制输出),BINARY(注解数据存储在二进制输出中, 但反射不可见), RUNTIME(注解数据存储在二进制输出中, 可用于反射 (默认值 ) 。
@Repeatable 允许在单个元素上多次使用同一个注解
@MustBeDocumented 表示这个注解是公开 API 的一部分, 在自动产生的 API 文档的类或者函数签名中, 应该包含这个注解的信息。

12.2.2 使用注解

上面我们声明了Run注解,它可以使用在CLASS,FUNCTION,VALUE_PARAMETER,EXPRESSION上。我们这里给出的示例是用在类上

@Run
class SwordTest {}

我们声明的 TestCase 注解,它有个构造函数,传入的参数是一个String类型的id。我们把这个注解用在函数上

@Run
class SwordTest {@TestCase(id = "1")fun testCase(testId: String) {println("Run SwordTest ID = ${testId}")}
}

上面是注解在代码中的简单使用。其中的 @TestCase(id = "1") 是注解的构造函数的使用。注解可以有带参数的构造器。注解参数的可支持数据类型如下:

  • 所有基本数据类型(Int,Float,Boolean,Byte,Double,Char,Long,Short)
  • String 类型
  • KClass 类型
  • enum 类型
  • Annotation 类型
  • 以上所有引用类型的数组(注意,不包括基本数据类型)

例如下面的都是合法的注解构造函数的参数类型

annotation class TestCase(val id: String)
annotation class TestCasee(val id: Int)
annotation class TestCaseee(val id: Array<String>)
annotation class TestCaseeee(val id: Run)
annotation class TestCaseeeeee(val id: KClass<String>)

而下面的两种声明编译不通过

annotation class TestCaseeeee(val id: Array<Int>)
annotation class TestCaseeeeee(val id: SwordTest)

另外,需要注意的是:注解类型不能有 null 类型,因为JVM不支持将null作为注解属性的值存储。如果注解用作另一个注解的参数,其名称不以@字符为前缀

annotation class AnnoX(val value: String)annotation class AnnoY(val message: String,val annoX: AnnoX = AnnoX("X"))

Java注解与Kotlin完全兼容。下面是一个Kotlin使用JUnit 4进行单元测试代码编写的例子

package com.easy.kotlinimport com.easy.kotlin.annotation.SwordTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4@RunWith(JUnit4::class)
class AnnotationClassNoteTest {@Testfun testAnno() {val sword = SwordTest()sword.testCase("10000")}
}

我们可以看出,除了@RunWith(JUnit4::class) 这地方的反射写法稍微有点不同外,剩下的跟我们在Java中使用 JUnit 的注解的方式基本上是一样的。

12.2.3 处理注解

定义了注解,并在需要的时候给相关类,类属性加上注解信息,如果没有相应的注解信息处理逻辑流程,那么注解可以说是废掉了,没啥实用价值。如何让注解在程序运行的时候发挥其特有的作用呢?核心就在于注解处理的代码了。本小节我们将学习到怎样进行注解信息的获取和处理。因为注解信息的获取主要是使用反射API,所以我们也会在本节中讲到反射相关的内容。

首先,我们的目标测试类是

@Run
class SwordTest {@TestCase(id = "1")fun testCase(testId: String) {println("Run SwordTest ID = ${testId}")}}

这里我们主要介绍 @TestCase 注解作用在函数上的处理过程。

::class 引用

首先,我们声明一个变量指向 SwordTest 对象实例

 val sword = SwordTest()

然后,我们就可以通过这个变量来获取此对象的类的信息。使用 ::class 来获取sword对象实例的 KClass 类的引用

val kClass = sword::class

上面的这行代码,Kotlin编译器会自动推断出kClass变量的类型是

val kClass:KClass<out SwordTest> = sword::class

这个KClass 数据类型我们将在下面的小节中介绍。

declaredFunctions 扩展属性

下面,我们需要获取sword对象类型所声明的所有函数。Kotlin中可以直接使用扩展属性 declaredFunctions 来获取这个类中声明的所有函数(对应的反射数据类型是 KFunction )。代码如下

val declaredFunctions = kClass.declaredFunctions

返回的是一个 Collection<KFunction<>> , 其中<> 是Kotlin泛型中的星投影,类似Java中的<?> 通配符。

这个 declaredFunctions 扩展属性的实现源码如下

@SinceKotlin("1.1")
val KClass<*>.declaredFunctions: Collection<KFunction<*>>get() = (this as KClassImpl).data().declaredMembers.filterIsInstance<KFunction<*>>()

annotations 属性

KFunction 类型继承了 KCallable , KCallable又继承了 KAnnotatedElement ,KAnnotatedElement 中有个 public val annotations: List<Annotation> 属性里面存储了该函数所有的注解的信息。通过遍历这个存储Annotation 的List,我们获取到 TestCase 注解

for (f in declaredFunctions) {// 处理 TestCase 注解,使用其中的元数据f.annotations.forEach {if (it is TestCase) {val id = it.id // TestCase 注解的属性 iddoSomething(id) // 注解处理逻辑}}
}

call 函数

另外,如果我们想通过反射来调用函数,可以直接使用 call 函数

f.call(sword, id)

上面的代码等价于 f.javaMethod?.invoke(sword, id) 。

到这里,我们就完成了一个简单的注解处理器。完整的代码如下

fun testAnnoProcessing() {val sword = SwordTest()// val kClasss:KClass<out SwordTest> = sword::class // 类型声明可省略val kClass = sword::classval declaredFunctions = kClass.declaredFunctions // 获取sword对象类型所声明的所有函数println(declaredFunctions)for (f in declaredFunctions) {// 处理 TestCase 注解,使用其中的元数据f.annotations.forEach {if (it is TestCase) {val id = it.iddoSomething(id) // 注解处理逻辑f.call(sword, id) // 等价于 f.javaMethod?.invoke(sword, id)}}}
}private fun doSomething(id: String) {println("Do Something in Annotation Processing ${id} ${Date()} ")
}@Target(AnnotationTarget.CLASS,AnnotationTarget.FUNCTION,AnnotationTarget.VALUE_PARAMETER,AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.RUNTIME)
@Repeatable
@MustBeDocumented
annotation class TestCase(val id: String)class SwordTest {@TestCase(id = "1")fun testCase(testId: String) {println("Run SwordTest ID = ${testId}")}}

测试代码

fun main(args: Array<String>) {testAnnoProcessing()
}

输出

[fun com.easy.kotlin.annotation.SwordTest.testCase(kotlin.String): kotlin.Unit]
Do Something in Annotation Processing 1 Mon Oct 23 23:04:09 CST 2017
Run SwordTest ID = 1

12.3 反射

在上面小节中的注解信息的获取与处理逻辑的实现中,其实我们已经用到了反射。反射是指在运行时(Run Time),程序可以访问、检测和修改它本身状态或行为的一种能力。Kotlin中的函数和属性也是头等公民,我们可以通过反射来内省属性和函数:如运行时属性名或类型,函数名或类型等。

在Kotlin中我们有两种方式来实现反射的功能。一种是调用Java 的反射包 java.lang.reflect 下面的API ,另外一种方式就是直接调用Kotlin语言提供的kotlin.reflect 包下面的API 。 不过因为反射功能的应用场景并非所有编程场景都用到,所有Kotlin把kotlin.reflect 包的实现放到了单独的kotlin-reflect-1.1.50.jar (当前版本号是1.1.50) 里面。所以在实际工程中,如果我们需要使用Kotlin的反射功能,以Gradle为例,需要在build.gradle配置文件中添加依赖

compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

Kotlin反射API类的层次结构如下图所示

Kotlin反射类图

12.3.1 类引用

为了方便讲解,我们先定义一个代码实例

open class BaseContainer<T>class Container<T : Comparable<T>> : BaseContainer<Int> {var elements: MutableList<T>constructor(elements: MutableList<T>) {this.elements = elements}fun sort(): Container<T> {elements.sort()return this}override fun toString(): String {return "Container(elements=$elements)"}}

反射是在运行时获取一个类引用。我们已经知道使用 ::class 调用可以获取到当前对象的 KClass对象

val container = Container(mutableListOf<Int>(1, 3, 2, 5, 4, 7, 6))
val kClass = container::class // 获取KClass对象

需要注意的是,Kotlin中类引用和Java中类引用是不同的,要获得java类的引用,可以直接使用 javaClass 这个扩展属性

val jClass = container.javaClass // 获取Java Class对象

javaClass 扩展属性在Kotlin中的实现源码是

public inline val <T: Any> T.javaClass : Class<T>@Suppress("UsePropertyAccessSyntax")get() = (this as java.lang.Object).getClass() as Class<T>

或者使用KClass实例的 .java 属性

val jkCLass  = kClass.java

这个KClass<T>.java 的扩展属性的实现源码如下

@Suppress("UPPER_BOUND_VIOLATED")
public val <T> KClass<T>.java: Class<T>@JvmName("getJavaClass")get() = (this as ClassBasedDeclarationContainer).jClass as Class<T>

12.3.2 函数引用

例如,我们有一个简单的判断一个Int整数是否是奇数的函数

fun isOdd(x: Int) = x % 2 != 0

我们可以代码中直接调用

>>> isOdd(7)
true
>>> isOdd(2)
false

另外,在高阶函数中我们想把它当做一个参数来使用,可以使用 :: 操作符

val nums = listOf(1, 2, 3)
val filteredNums = nums.filter(::isOdd)
println(filteredNums) // [1, 3]

这里的 ::isOdd 就是一个函数类型 (Int) ->Boolean 的值 。

12.3.3 属性引用

在Kotlin中,访问属性是属于第一级对象,我们可以使用 :: 操作符

var one = 1
fun testReflectProperty() {println(::one.get()) // 1::one.set(2)println(one)         // 2
}fun main(args: Array<String>) {testReflectProperty()
}

表达式 ::one 等价于类型为KProperty的一个属性,它可以允许我们通过 get 函数获取值 ::one.get() 。

对于可变属性 var one = 1 ,返回类型为KMutableProperty的值,并且还有set方法 ::one.set(2) 。

12.3.4 绑定函数和属性引用

我们可以引用一个对象实例的方法。例如下面的代码

val digitRegex = "\\d+".toRegex()
digitRegex.matches("7") // true
digitRegex.matches("6") // true
digitRegex.matches("5") // true
digitRegex.matches("X") // false

其中的 digitRegex.matches 重复出现,显得“样板化”。 在Kotlin中可以直接引用digitRegex对象实例的matches方法。上面的代码我们可以写成下面这样

val isDigit = digitRegex::matches  // 引用 digitRegex 对象实例的 matches 方法
isDigit("7")// true
isDigit("6")// true
isDigit("5")// true
isDigit("X")// true

是不是很酷? 真的是相当简洁。

12.4 使用反射获取泛型信息

在Java中,使用反射的一个代码实例如下

package com.easy.kotlin;import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;interface StudentService<T> {List<T> findStudents(String name, Integer age);
}public class ReflectionDemo {public static void main(String[] args) {StudentServiceImpl studentService = new StudentServiceImpl();studentService.save(new Student("Bob", 20));studentService.findStudents("Jack", 20);// 反射API调用示例final Class<? extends StudentServiceImpl> studentServiceClass = studentService.getClass();Class<?>[] classes = studentServiceClass.getDeclaredClasses();Annotation[] annotations = studentServiceClass.getAnnotations();ClassLoader classLoader = studentServiceClass.getClassLoader(); // Returns the class loader for the classField[] fields = studentServiceClass.getDeclaredFields(); // 获取类成员变量Method[] methods = studentServiceClass.getDeclaredMethods(); // 获取类成员方法try {methods[0].getName(); // savemethods[0].invoke(studentService, "Jack",20);} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}}class StudentServiceImpl extends BaseService<Student> implements StudentService<Student> {public List<Student> findStudents(String name, Integer age) {return Arrays.asList(new Student[] {new Student("Jack", 20), new Student("Rose", 20)});}@Overridepublic int save(Student student) {return 0;}
}abstract class BaseService<T> {abstract int save(T t);
}class Student {String name;Integer age;public Student(String name, Integer age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}
}

通过反射,我们可以获取一个类的注解、方法、成员变量、方法等等。那么我们能不能通过反射获取到泛型的信息呢?我们知道 Java中的泛型采用擦拭法。在运行时,无法得到自己本身的泛型信息。而当这个类继承了一个父类,父类中有泛型的信息,那么我们可以通过调用getGenericSuperclass()方法得到父类的泛型信息。getGenericSuperclass()是Generic继承的特例,对于这种情况子类会保存父类的Generic参数类型,返回一个ParameterizedType。另外,我们所说的 Java 泛型在字节码中会被擦除,并不总是擦除为 Object 类型,而是擦除到上限类型。

在Kotlin也是一样的泛型机制。所以,通过反射能拿到的也只能是有继承父类泛型信息的子类泛型。

class A<T>open class C<T>
class B<T> : C<Int>()  // 继承父类 C<Int>()  fun fooA() {// 无法在此处获得运行时 T 的具体类型!!!运行报错:java.lang.Class cannot be cast to java.lang.reflect.ParameterizedTypeval parameterizedType = A<Int>()::class.java.genericSuperclass as ParameterizedTypeval actualTypeArguments = parameterizedType.actualTypeArgumentsfor (type in actualTypeArguments) {val typeName = type.typeNameprintln("typeName = ${typeName}")}
}fun fooB() {// 当继承了父类 C<Int> 的时候,在此处获得运行时 genericSuperclass T 的具体类型val parameterizedType = B<Int>()::class.java.genericSuperclass as ParameterizedTypeval actualTypeArguments = parameterizedType.actualTypeArgumentsfor (type in actualTypeArguments) {val typeName = type.typeNameprintln("typeName = ${typeName}") // typeName = java.lang.Integer}
}fun main(args: Array<String>) {// fooA() fooB()
}

下面我们通过一个简单的实例来说明Kotlin中的反射怎样获取泛型代码的基本信息。

首先,声明一个父类 BaseContainer<T>

open class BaseContainer<T>

然后,声明一个 Container<T : Comparable<T>> 继承它

class Container<T : Comparable<T>> : BaseContainer<Int> {var elements: MutableList<T>constructor(elements: MutableList<T>) {this.elements = elements}fun sort(): Container<T> {elements.sort()return this}override fun toString(): String {return "Container(elements=$elements)"}
}

声明一个 Container对象实例

val container = Container(mutableListOf<Int>(1, 3, 2, 5, 4, 7, 6))

获取container 的KClass 对象引用

val kClass = container::class // 获取KClass对象

KClass对象的 typeParameters 属性中存有类型参数的信息

val typeParameters = kClass.typeParameters // 获取类型参数typeParameters信息,也即泛型信息val kTypeParameter: KTypeParameter = typeParameters[0]
println(kTypeParameter.isReified) // false
println(kTypeParameter.name) // T
println(kTypeParameter.upperBounds) // [kotlin.Comparable<T>]
println(kTypeParameter.variance) // INVARIANT

KClass的 constructors 属性中存有构造函数的信息,我们可以从中获取构造函数的入参等信息

val constructors = kClass.constructors
for (KFunction in constructors) {KFunction.parameters.forEach {val name = it.nameval type = it.typeprintln("name = ${name}") // elementsprintln("type = ${type}") // kotlin.collections.MutableList<T>for (KTypeProjection in type.arguments) {println(KTypeProjection.type) // T}}
}

本章小结

第12章 元编程与注解、反射相关推荐

  1. 《JavaScript权威指南第7版》第14章 元编程

    第14章 元编程 14.1 属性特性 (Property Attributes) 14.2 对象扩展性 14.3 prototype特性(原型特性) 14.4 内置Symbol 14.4.1 Symb ...

  2. 《深入理解计算机系统》读书笔记-016(第 12 章 并发编程)

    <深入理解计算机系统>读书笔记-016(第 12 章 并发编程) 太惨了,这章真心不大看得懂啊--等把前面的补上之后把读书笔记重新整理一下吧.这样看了跟没看也没啥区别了. 在线程中,不同于 ...

  3. CSAPP:第12章 并发编程

    CSAPP:第12章 并发编程 文章目录 CSAPP:第12章 并发编程 12.1 基于进程的并发编程(Process-based) 12.1.1 基于进程的并发服务器 12.1.2 进程的优劣 12 ...

  4. 深入理解计算机系统——第12章 并发编程

    深入理解计算机系统--第12章 并发编程 并发编程 如果逻辑控制流在时间上重叠,那么就称它们是并发的.注意:核心是在时间上重叠. 操作系统内核运行多个应用程序采用了并发机制,但并发不止用于内核,也用于 ...

  5. 第12章 GUI编程与Tkinter相关组件介绍

    本章的知识点: 1.了解GUI程序开发: 2.学习Tkinter的主要组件 内容: 12.1 GUI程序开发简介 12.2 Tkinter与主要组件 创建和运行GUI程序,需要5步: 1.导入Tkin ...

  6. 【Groovy】编译时元编程 ( 利用注解进行 AST 语法树转换 | 定义注解并使用 GroovyASTTransformationClass 注明 AST 转换接口 | AST 转换接口实现 )

    文章目录 一.利用注解进行 AST 语法树转换 1.定义注解并使用 GroovyASTTransformationClass 注明 AST 转换接口 2.AST 转换接口实现 3.定义 Groovy ...

  7. java元编程_一文读懂元编程

    元编程(Metaprogramming)是编写.操纵程序的程序,简而言之即为用代码生成代码.元编程是一种编程范式,在传统的编程范式中,程序运行是动态的,但程序本身是静态的.在元编程中,两者都是动态的[ ...

  8. Elixir元编程-第三章 编译时代码生成技术进阶

    Elixir元编程-第三章 编译时代码生成技术进阶 注:本章内容来自 Metaprogramming Elixir 一书,写的非常好,强烈推荐.内容不是原文照翻,部分文字采取意译,主要内容都基本保留, ...

  9. 读书笔记:《流畅的Python》第21章 类元编程

    # 第21章 类元编程""" 类元编程指的是运行时创建或定制类的技艺1.类是一等对象,任何时候都可以使用函数新建类,而无需使用class关键字2.类装饰器也是函数,不过能 ...

最新文章

  1. 系统调用和库函数调用的区别
  2. 西点军校最贵一课:没强大内心的人,没资格谈人生
  3. 12.6 Nginx安装 12.7 默认虚拟主机 12.8 Nginx用户认证 12.9 Nginx
  4. 某银行省级数据中心IT运维服务体系建设完整思路
  5. xlrd.biffh.XLRDError: Excel xlsx file; not supported报错
  6. jdk1.8要安装什么mysql_Window下安装JDK1.8+Tomcat9.0.27+Mysql5.7.28的教程图解
  7. Angle Admin Template介绍
  8. influxdb数据备份和恢复
  9. 2-Eighteenth Scrum Meeting-20151218
  10. php一对一模型关联,通过实例学习Laravel模型中的一对一关联关系
  11. linux DNS安装配置
  12. “中国工程设计大师”俞加康:为地铁耕耘“时不我待,只争朝夕”
  13. 【Maven】Maven下载源码和Javadoc的方法
  14. 遍历Panel1中所有label控件的Text
  15. css媒体查询和居中
  16. STM32——库函数版——独立按键程序
  17. 山东大学高频电子线路综合实验 调幅通信机系统实验详解
  18. o2o电商模式的创业机会有哪些?
  19. linux系统制作macos启动,如何手动制作macOS High Sierra可启动安装U盘
  20. VB写的软件加壳都没用,超强反调试反破解分析,检测OD调试器

热门文章

  1. 1037. 在霍格沃茨找零钱(20)-PAT乙级真题
  2. 到目前为止,Linux下最完整的Samba服务器配置攻略
  3. Git for Windows安装和基本设置
  4. ibatis sql_Map中出现异常:Cause: java.lang.RuntimeException: JavaBeansDataExchange could not instantiate..
  5. zabbix 监控percona
  6. NSString 转为gbk
  7. java.util.concurrent包(6)——CyclicBarrier使用
  8. 如何进行Android单元测试
  9. vmware Horizon View 5.2初体验(三)——composer安装
  10. 如何写代码,才能越写越轻松?