内容简介:可是以上四种
Block 简介
-
block是将函数及其执行上下文封装起来的一个对象 - 在
block实现的内部,有很多变量,因为block也是一个对象 - 其中包含了诸如
isa指针,imp指针等对象变量,还有储存其截获变量的对象等
定义和使用
block 根据有无参数和有无返回值有以下几种简单使用方式
// 无参数无返回值
void (^ BlockOne)(void) = ^(void){
NSLog(@"无参数,无返回值");
};
BlockOne();//block的调用
// 有参数无返回值
void (^BlockTwo)(int a) = ^(int a){
NSLog(@"有参数,无返回值, 参数 = %d,",a);
};
BlockTwo(100);
// 有参数有返回值
int (^BlockThree)(int,int) = ^(int a,int b){
NSLog(@"有参数,有返回值");
return a + b;
};
BlockThree(1, 5);
// 无参数有返回值
int(^BlockFour)(void) = ^{
NSLog(@"无参数,有返回值");
return 100;
};
BlockFour();
可是以上四种 block 底层又是如何实现的呢? 其本质到底如何? 接下来我们一起探讨一下
Block的本质
- 为了方便我们这里新建一个
Command Line Tool项目, 在main函数中执行上述中一个block - 探索
Block的本质, 就要查看其源码, 这里我们使用下面命令把main.m文件生成与其对应的c++代码文件 - 在
main.m文件所在的目录下, 执行如下命令, 会生成一个main.cpp文件 - 把
main.cpp文件添加到项目中, 并使其不参与项目的编译, 下面我们就具体看一下block的底层到底是如何实现的
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
打开 main.cpp 文件, 找到文件最底部, 可以看到 block 的相关源码如下
// block的结构体
struct __main_block_impl_0 {
// 结构体的成员变量
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_11c959_mi_0);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 定义block变量
void (* BlockOne)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 执行block内部的源码
((void (*)(__block_impl *))((__block_impl *)BlockOne)->FuncPtr)((__block_impl *)BlockOne);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
其中 block 的声明和调用的对应关系如下
删除其中的强制转换的相关代码后
// 定义block变量
void (* BlockOne)(void) = &__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA
);
// 执行block内部的源码
BlockOne->FuncPtr(BlockOne);
上述代码中 __main_block_impl_0 函数接受两个参数, 并有一个返回值, 最后把函数的地址返回给 BlockOne , 下面找到 __main_block_impl_0 的定义
// 结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// c++中的构造函数, 类似于OC中的init方法
// flags: 默认参数, 调用时可不传
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
-
__main_block_impl_0函数中的第一个参数__main_block_func_0赋值给了fp,fp又赋值给了impl.FuncPtr, 也就意味着impl.FuncPtr中存储的就是我们要执行的__main_block_func_0函数的地址 -
Block结构体中的isa指向了_NSConcreteStackBlock, 说明Block是一个_NSConcreteStackBlock类型, 具体后面会详解 -
__main_block_impl_0函数中的第二个参数__main_block_desc_0_DATA
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
- 其中
reserved赋值为0 -
Block_size被赋值为sizeof(struct __main_block_impl_0), 即为__main_block_impl_0这个结构体占用内存的大小 -
__main_block_impl_0的第二个参数, 接受的即为__main_block_desc_0结构体的变量(__main_block_desc_0_DATA)的地址
Block变量捕获
- 局部变量分为两大类:
auto和static-
auto: 自动变量, 离开作用域就会自动销毁, 默认情况下定义的局部变量都是auto修饰的变量, 系统都会默认给添加一个auto -
auto不能修饰全局变量, 会报错 -
static作用域内修饰局部变量, 可以修饰全局变量
-
- 全局变量
局部变量
auto变量捕获
auto 局部变量在 Block 中是值传递
下述代码输出值为多少?
int age = 10;
void (^BlockTwo)(void) = ^(void){
NSLog(@"age = %d,",age);
};
age = 13;
BlockTwo();
// 输出10
输出值为什么是10而不是13呢? 我们还是生成 main.cpp 代码看一下吧, 相关核心代码如下
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 这里多了一个age属性
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_80d62b_mi_0,age);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 定义属性
int age = 10;
// block的定义
void (*BlockTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 改变属性值
age = 13;
// 调用block
((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
那么下面我们一步步看一下, 吧一些强制转换的代码去掉之后
int age = 10;
void (*BlockTwo)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age
);
age = 13;
BlockTwo->FuncPtr(BlockTwo);
在上面的 __main_block_impl_0 函数里面相比于之前的, 多了一个 age 参数
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 新的属性age
int age;
// 构造函数, 多了_age参数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 上面的构造方法
__main_block_impl_0中, 多了一个_age参数 - 同时后面多了一条
age(_age)语句, 在c++中,age(_age)相当于age = _age, 即给age属性赋值, 存储构造函数传过来的age属性的值 - 所以在后面调用
block的时候,block对应的结构体所存储的age属性的值仍然是10, 并没有被更新
// 及时这里重新对age进行了赋值
age = 13;
// 这里调用BlockTwo的时候, 结构体重的age属性的值并没有被更新
BlockTwo->FuncPtr(BlockTwo);
// 最后在执行block内部逻辑的时候,
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
// 这里的age, 仍然是block结构体中的age, 值并没有改变, 所以输出结果还是10
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_80d62b_mi_0,age);
}
static变量捕获
static 局部变量在 Block 中是指针传递, 看一下下面代码的输出情况
auto int age = 10;
static int weight = 20;
void (^BlockTwo)(void) = ^(void){
NSLog(@"age = %d, weight = %d,",age, weight);
};
age = 13;
weight = 23;
BlockTwo();
age = 10, weight = 23 age weight main.cpp
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *weight;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *weight = __cself->weight; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_282a93_mi_0,age, (*weight));
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int weight = 20;
void (*BlockTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));
age = 13;
weight = 23;
((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
- 从上面代码可以看到
__main_block_impl_0类中多了两个成员变量age和weight, 说明两个变量我们都可以捕获到 - 不同的是, 同样都是
int变量, 使用不同的修饰词修饰,__main_block_impl_0类中也是不同的 -
static修饰的变量weight在block中存储的是weight的地址, 在后面的block函数中我们使用的也是其地址
int age;
int *weight;
// &weight
void (*BlockTwo)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age, &weight);
// 下面构造方法中, 同样(weight(_weight)方法之前讲过)将传过来的weight的地址赋值给了 (int *weight;)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
- 也就是说上面的构造函数中
-
age保存的是一个准确的值 -
weight保存的是weight所在的内存地址
-
- 所以在最后调用
block内部逻辑的时候
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *weight = __cself->weight; // bound by copy
// (*weight)相当于从weight的内存地址中取值, 在执行操作
// 然而weight内存中的值已经在后面赋值的时候被更新了, 所以这里取出的值是赋值后的
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_282a93_mi_0,age, (*weight));
}
- 也就是说, 同样是局部变量
-
auto修饰的变量在block中存储的是变量的值(值传递) -
static修饰的变量在block中存储的是变量的内存地址(地址传递)
全局变量
int age = 10;
static int weight = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^BlockTwo)(void) = ^(void){
NSLog(@"age = %d, weight = %d,",age, weight);
};
age = 13;
weight = 23;
BlockTwo();
}
return 0;
}
上面代码的输出结果, 毫无疑问是13和23, 相关 c++ 代码如下
int age = 10;
static int weight = 20;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 封装了block执行逻辑的函数
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_0ee0bb_mi_0,age, weight);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 定义block变量
void (*BlockTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
age = 13;
weight = 23;
((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
- 从上面代码可以看出
__main_block_impl_0结构体重并没有捕获到age和weight的成员变量 - 同样在定义
block变量的时候中也不需要传入age和weight的变量 - 在封装了
block执行逻辑的函数中, 就可以直接使用全局的变量即可
Block的类型
Block的三种类型
- 在之前的
C++源码中,__main_block_impl_0结构体中isa指向的类型是_NSConcreteStackBlock - 下面就具体看一下,
Block的只要类型有那些 - 先看一下下面这部分代码的输出结果
void (^block)(void) = ^(void){
NSLog(@"Hello World");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
/*
2019-06-24 15:46:32.506386+0800 Block[3307:499032] __NSGlobalBlock__
2019-06-24 15:46:32.506578+0800 Block[3307:499032] __NSGlobalBlock
2019-06-24 15:46:32.506593+0800 Block[3307:499032] NSBlock
2019-06-24 15:46:32.506605+0800 Block[3307:499032] NSObject
*/
-
block的类型NSBlock最终也是继承自NSObject - 这也可以解释为什么
block的结构体__main_block_impl_0中会有一个isa指针了 - 此外,
block共有三种类型, 可以通过调用class方法或者isa指针查看具体类型, 最终都是继承自NSBlock类型-
__NSGlobalBlock__或者_NSConcreteGlobalBlock -
__NSStackBlock__或者_NSConcreteStackBlock -
__NSMallocBlock__或者_NSConcreteMallocBlock
-
block在内存中的分配
_NSConcreteGlobalBlock _NSConcreteStackBlock _NSConcreteMallocBlock
- 应用程序的内存分配图如上图所示, 自上而下依次为内存的低地址–>内存的高地址
- 程序区域: 代码段, 用于存放代码
- 数据区域: 数据段, 用于存放全局变量
- 堆: 动态分配内存,需要 程序员 自己申请,程序员自己管理, 通常是
alloc或者malloc方式申请的内存 - 栈: 用于存放局部变量, 系统会自动分配内存, 自动销毁内存
区分不同的block类型
- 上面提到, 一共有三种
block类型, 且不同的block类型存放在内存的不同位置 - 但是如何区分所定义的
block
到底是哪一种类型呢
看看下面代码的执行情况, 运行环境实在MRC环境下
static int age = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int weight = 21;
void (^block1)(void) = ^(void){
NSLog(@"Hello World");
};
void (^block2)(void) = ^(void){
NSLog(@"age = %d", age);
};
void (^block3)(void) = ^(void){
NSLog(@"age = %d", weight);
};
NSLog(@"block1 = %@", [block1 class]);
NSLog(@"block2 = %@", [block2 class]);
NSLog(@"block3 = %@", [block3 class]);
/*
2019-06-24 21:13:14.555206+0800 Block[30548:1189724] block1 = __NSGlobalBlock__
2019-06-24 21:13:14.555444+0800 Block[30548:1189724] block2 = __NSGlobalBlock__
2019-06-24 21:13:14.555465+0800 Block[30548:1189724] block3 = __NSStackBlock__
*/
}
return 0;
}
针对各种不同的 block 总结如下
block 类型 |
环境 |
|---|---|
__NSGlobalBlock__ |
没有访问auto变量 |
__NSStackBlock__ |
访问了auto变量 |
__NSMallocBlock__ |
__NSStackBlock__ 调用了copy |
- 由于
__NSMallocBlock__是放在堆区域 - 要想创建出
__NSMallocBlock__类型的block, 我们可以调用copy方法
void (^block3)(void) = ^(void){
NSLog(@"age = %d", weight);
};
NSLog(@"block3 = %@", [block3 class]);
NSLog(@"block3 = %@", [[block3 copy] class]);
/* 输出分别是:
block3 = __NSStackBlock__
block3 = __NSMallocBlock__
*/
- 从上面的代码中我们可以明显看到,
__NSStackBlock__类型的block调用copy方法后, 就会变成__NSMallocBlock__类型的block - 相当于生成的
block是在堆区域的 - 那么另外两种类型调用
copy方法后,又会如何? 下面一起来看一下吧
int weight = 21;
void (^block1)(void) = ^(void){
NSLog(@"Hello World");
};
void (^block3)(void) = ^(void){
NSLog(@"age = %d", weight);
};
NSLog(@"block1 = %@", [block1 class]);
NSLog(@"block1 = %@", [[block1 copy] class]);
NSLog(@"block3 = %@", [block3 class]);
NSLog(@"block3 = %@", [[block3 copy] class]);
NSLog(@"block3 = %@", [[[block3 copy] copy] class]);
/*
__NSGlobalBlock__
__NSGlobalBlock__
__NSStackBlock__
__NSMallocBlock__
__NSMallocBlock__
*/
- 从上面的代码可以看到, 只有
__NSStackBlock__类型的block调用copy之后才会变成__NSMallocBlock__类型, 其他的都是原类型 - 主要也是
__NSStackBlock__类型的作用域是在栈中, 作用域中的局部变量会在函数结束时自动销毁 -
__NSStackBlock__调用copy操作后,分配的内存地址相当于从栈复制到堆;副本存储位置是堆 - 其他的则可参考下面表格
| Block类 | 副本源的配置存储域 | 复制效果 |
|---|---|---|
__NSStackBlock__ |
栈 | 从栈复制到堆 |
__NSGlobalBlock__ |
程序的数据区域 | 什么也不做 |
__NSMallocBlock__ |
堆 | 引用计数增加 |
- 在
ARC环境下, 编译器会根据情况自动将站上的block复制到堆上, 类似以下情况-
block作为函数返回值时 - 将
block赋值给__strong修饰的指针时 -
block作为GCD的方法参数时
-
__block修饰符
Question: 定义一个auto修饰的局部变量, 并在 block 中修改该变量的值, 能否修改成功呢?
auto int width = 10;
static int height = 20;
void (^block)(void) = ^(void){
// 事实证明, 在Xcode中这行代码是报错的
width = 22;
// 但是static修饰的变量, 却是可以赋值, 不会报错
height = 22;
NSLog(@"width = %d, height = %d", width, height);
};
block();
// width = 10, height = 22
- 在之前提到, 在
block中,auto修饰的变量是值传递 -
static修饰的变量是指针传递, 所以在上述代码中,block存储的只是height的内存地址 - 同样
auto变量实在main函数中定义的, 而block的执行逻辑是在__main_block_func_0结构体的方法中执行的, 相当于局部变量不能跨函数访问 - 至于
static修饰的变量为什么可以修改?- 在
__main_block_impl_0结构体中height存储的是其内存地址, 在其他函数或者结构体中访问和改变height的方式都是通过其真真访问的 - 类似赋值方式:
(*height) = 22; - 取值方式:
(*height)
- 在
__block修饰auto变量
__block auto int width = 10;
void (^block)(void) = ^(void) {
// 很明显, 这里就可以修改了
width = 12;
NSLog(@"width = %d", width);
};
block();
// width = 12
为什么上面的代码就可以修改变量了呢, 这是为什么呢…..请看源码
下面是生成的 block 的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 这里的width被包装成了一个__Block_byref_width_0对象
__Block_byref_width_0 *width; // by ref
// 这里可以对比一下之前的未被__block修饰的int变量
// int width;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_width_0 *_width, int flags=0) : width(_width->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 上述代码看到
__block可以用于解决block内部无法修改auto修饰的变量值得问题 - 但是
__block不能修饰全局变量和static修饰的静态变量(同样也不需要, 因为在block内部可以直接修改) - 经过
__block修饰的变量会被包装成一个对象(__Block_byref_width_0) - 下面是
width被包装后的对象的结构体, 在结构体内, 会有一个width成员变量
struct __Block_byref_width_0 {
void *__isa;
// 一个指向自己本身的成员变量
__Block_byref_width_0 *__forwarding;
int __flags;
int __size;
// 外部定义的auto变量
int width;
};
下面我们先看一下, auto 和 block 的定义和调用
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// __block auto int width = 10;
auto __Block_byref_width_0 width = {
0,
&width,
0,
sizeof(__Block_byref_width_0),
10
};
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
&width,
570425344
);
block->FuncPtr(block);
}
return 0;
}
- 可以看到在定义的
__Block_byref_width_0类型的width中的每一个参数分别赋值给了__Block_byref_width_0结构体中的每一个成员变量 - 而在
block内部重新对width重新赋值的逻辑中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_width_0 *width = __cself->width; // bound by ref
(width->__forwarding->width) = 12;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_9241d5_mi_0, (width->__forwarding->width));
}
- 上面代码中的
width是一个__Block_byref_width_0类型的变量 -
width对象通过找到内部的__forwarding成员变量 - 在
__Block_byref_width_0结构体中__forwarding是一个指向自己本身的成员变量 - 所以最后再通过
__forwarding找到__Block_byref_width_0的成员变量width, 在进行重新赋值 - 在
NSLog中也是通过这种逻辑获取width的值
以上所述就是小编给大家介绍的《浅谈OC中Block的本质》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mastering Regular Expressions, Second Edition
Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95
Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!