iOS——【Blocks】

Blocks概要

Blocks是C语言的扩充功能,即带有自动变量的匿名函数。匿名函数就是不带函数名的函数。这一概念同样被称为“闭包”,lambda计算等。
自动变量是在函数内部声明的变量,其作用域仅限于声明它的函数内部。这意味着它们只能在其声明的函数内部使用,并且在函数执行完毕后会被自动销毁。

Blocks模式

block语法的完整形式:

^void (int event) {//...
}

即为:

^返回值类型 参数列表 表达式
与C语言不同的地方有:

  1. 没有“^”(插入记号):插入该记号便于查找。
  2. 没有函数名:因为为匿名函数。

block的返回值类型是可以省略的,省略返回值类型后,如果有return语句就返回该返回值类型,没有的话就使用void类型。
其次,如果不使用参数,参数列表也可以省略。

Block类型变量

同样的,在Block语法下,可将Block语法赋值给声明为Block类型的变量中。在有关Blocks的文档中,“Block”既指源代码中的Block语法,也指由Blcok语法所生成的值。
声明Block类型变量的语法如下:

int (^blk)(int);

该Block类型变量与一般的C语言变量完全相同,可以用于:自动变量、函数参数、静态变量、静态全局变量、全局变量。
下面使用Block语法将Block赋值为Block变量:

int (^blk)(int) = ^(int count)(return count+1);

由“^”开始的Block语法生成的Block被赋值给变量blk中。因为与通常的变量相同,所以也可以由Block类型变量向Block类型变量赋值。

int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;

在函数参数中使用Block类型变量可以向函数传递Block:

void func (int (^blk)(int)) {}

在函数返回值中指定Block类型,可以将Block作为返回值返回:

int (^func()(int)) {return ^(int count)(return count+1);
}

还可以使用typedef简化块的记述方式,见EOF学习的博客第38条。
将赋值给Block类型变量的Block方法像C语言通常的函数调用那样使用,这种方法与使用函数指针类型变量调用函数的方法几乎完全相同。变量funcptr为函数指针类型的时候,像下面这样调用函数指针类型变量:

int result = (*funcptr)(10);

变量blk为Block类型的情况下,这样调用Block类型变量:

int result = blk(10);

通过Block类型变量调用Block与C语言通常的函数调用没有区别:

// blk_t blk就是一个块对象
int func(blk_t blk, int rate) {return blk(rate);
}//块对象在OC中也可以当参数
- (int) useBlock: (blk_t)blk rate:(int)rate;

也可以使用指向Block类型变量的指针,即Block指针类型变量。

typedef int (^blk_t)(int);
blk_t blk = ^(int count)(return count+1);
blk_t *blkptr = &blk;
(*blktr)(10);

截获自动变量值

Blocks中,Blocks表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。
因为Block表达式保存了自动变量的值,所以在执行Blocks语法后,即使改写Block中所使用的自动变量的值也不会影响执行时自动变量的值。

int main() {int dmy = 256;int val = 10;const char *fmt = “val = %d\n”;//这里声明了块就是在截获变量,此时捕获的fmt和val的值就是在该块被创建之前那一瞬间的值,哪怕后面已经改变了fmt和val的值,这里截获的结果还是改变之前的值,因为那才是这个块创建那一瞬间的时候变量的值。void(^blk)(void) = ^{printf(fmt, val);};val = 2;fmt = “These value were changed. val = %d\n”;blk();return 0;
}

__block说明符

实际上,自动变量值截获只能保存执行Block语法瞬间的值。保存后就不能修改该值。如果尝试改写截获的自动变量的值:

int val = 0;
void (^blk)(void) = ^{val = 1};

我们发现编译的时候会报错。
若想在Block语法表达式中给自动变量赋值,需要在该自动变量上附加__block说明符:

__block int val = 0;
void (^blk)(void) = ^{val = 1};

使用附有_ _blcok说明符的可在Block中赋值,该变量称为__block变量。

截获的自动变量

已知如果尝试改写截获的自动变量的值,编译的时候会报错。那么截获OC对象,调用变更该对象的方法也会产生编译错误吗?

id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{id obj = [[NSObject alloc] init];[array addObject: obj];
}

这样是没有问题的,而向截获的变量array赋值就会编译错误。
虽然向截获的变量array赋值会产生错误,但是使用截获的值不会产生错误。
这种情况下,需要给截获的自动变量附加 _block说明符。

__block id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{array = [[NSMutableArray alloc] init];
};

Blocks的实现

Block的实质

clang (LLVM 编译器)具有转换为我们可读源代码的功能 。通过“-rewrite-objo”选项就能将含有Block语法的
源代码变换为C ++的源代码。说是C ++,其实也仅是使用 了str uc t 结构,其本质是C 语言源代码。

clang -rewrite-objc 源代码文件名

我们转换如下的block代码:

int main() {void (^blk)(void) = ^{printf("Block\n”);};blk(); return 0;
}

此代码的Block语法最为简单,它省略了返回值类型以及参数列表。该源代码通过clang可变换为以下形式:

//经过clang转换后的C++代码
struct __block_impl {void *isa; // 指向 block 的类的指针int Flags; // 标志位int Reserved; // 保留字段void *FuncPtr; // 指向 block 函数的指针
};struct __main_block_impl_0 {struct __block_impl impl; // block 的实现struct __main_block_desc_0 *Desc; // block 的描述// 构造函数,初始化 impl 和 Desc 字段__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock; // 设置 isa 指针impl.Flags = flags; // 设置标志位impl.FuncPtr = fp; // 设置函数指针Desc = desc; // 设置描述指针}
};// block 函数的实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("Block\n"); // 打印信息
}// block 的描述结构
static struct __main_block_desc_0 {size_t reserved; // 保留字段size_t Block_size; // block 的大小
} __main_block_desc_0_DATA = {0, // 保留字段为 0sizeof(struct __main_block_impl_0) // block 实现的大小
};int main(int argc, const char * argv[]) {// 定义一个函数指针 blk,指向 __main_block_impl_0 结构体的实例,该实例通过 __main_block_impl_0 构造函数初始化void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);// 调用 block 函数指针((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);return 0; // 返回
}

struct _ _block_impl:这个结构体定义了一个 block 的实现,包含了指向 block 类的指针 isa、标志位 Flags、保留字段 Reserved 和指向 block 函数的指针 FuncPtr。
struct _ _main_block_impl_0:这个结构体扩展了 __block_impl,定义了一个特定的 block 实现。它包含一个 _ _block_impl 的实例、一个指向 block 描述的指针 Desc,并且有一个构造函数用于初始化这些字段。
_ _main_block_impl_0(void *fp, struct _ _main_block_desc_0 *desc, int flags=0):这是 _ _main_block_impl_0 的构造函数,用于初始化 impl 和 Desc 字段。
_ _main_block_func_0(struct _ main_block_impl_0 * _cself):这是 block 的实际函数实现,在本例中只是简单地打印 “Block” 信息。
struct _ _main_block_desc_0:这个结构体描述了 block 的大小和保留空间。
_ _main_block_desc_0_DATA:这是 block 描述的实际数据,包括大小信息。
main 函数中,首先定义了一个函数指针 blk,它指向一个 _ _main_block_impl_0 结构体的实例,该实例通过 _ main_block_impl_0 的构造函数初始化,并且调用了这个 block 函数指针,打印 “Block” 信息。
该函数的参数
cself相当于C++实例方法中所指的自身变量this,或是OC实例方法中指向对象自身的变量self,即参数 _cself为指向Block值的变量。

由这次Block语法变换而来的_main_block_func_0 函数并不使用__cself。我们先来看看该参数的声明:

struct __main_block_impl_0* __cself//结构体声明:
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;
}

第一个成员变量是impl,其__block_impl结构体的声明:

struct __block_impl {void *isa;int Flags;int Reserved;void *FuncPtr;
}

第二个成员变量是Desc指针,其__main_block_desc_0结构体的声明:

static struct __main_block_desc_0 {size_t reserved;size_t Block_size;
}

以上就是初始化__main_block_impl_0结构体成员的源代码。我们刚刚跳过了_NSConcreteStackBlock的说明。_NSConcreteStackBlock用于初始化__block_impl结构体的isa成员。虽然大家很想了解它,但在进行讲解之前,我们先来看看该构造函数的调用。

void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

因为转换较多,看起来不是很清楚,所以我们去掉转换的部分,具体如下:

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);struct __main_block_impl_0 *blk = &tmp;

这样就容易理解了。该源代码将_ main_block_impl0结构体类型的自动变量,即栈上生成的 main_block_impl_0 结构体实例的指针,赋值给 _main_block_impl_0结构体指针类型的变量 blk。以下为这部分代码对应的最初源代码。

void(^blk)(void)=^{printf("Block\n");};

将 Block 语法生成的Block赋给Block 类型变量blk。它等同于将_ main_block_impl_0 结构体实例的指针赋给变量blk。该源代码中的 Block 就是 main_block_impl_0 结构体类型的自动变量,即栈上生成的 main_block_impl_0结构体实例。
下面就来看看
_main_block_impl_0结构体实例构造参数。

__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

第一个参数是由Block语法转换的C语言函数指针。第二个参数是作为静态全局变量初始化的_ main_block_desc_0 结构体实例指针。以下为 _main_block_desc_0 结构体实例的初始化部分代码。

static struct __main_block_desc_0 __main_block_desc_0_DATA = {0sizeof(struct __main_block_impl_0)
};

我们来确认一下使用该 Block的部分。

blk();

这部分可变换为以下源代码:

((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct_block_impl *)blk);

去掉转换部分。

(*blk->impl.FuncPtr)(blk);

这就是简单地使用函数指针调用函数。正如我们刚才所确认的,由Block 语法转换的_ main_block_func_0函数的指针被赋值成员变量FuncPtr中。另外也说明了, _main_block_func_0函数的参数__cself指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行了传递。

其实,所谓Block 就是Objective-C 对象。

截获自动变量

int main(int argc, const char * argv[]) {int dmy = 256;int val = 10;const char  *fmt = "val = %d\n";void (^blk)(void) = ^{printf(fmt, val);};blk();return 0;
}
struct __block_impl {void *isa;int Flags;int Reserved;void *FuncPtr;
};struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;const char *fmt;int val;__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};static void __main_block_func_0(struct __main_block_impl_0 *__cself {const char *fmt = __cself->fmt;int val = __cself->val; printf(fmt, val);
}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[]) {int dmy = 256;int val = 10;const char *fmt = "val = %d\n";void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));return 0;
}

其中Block语法表达式中使用的自动变量被作为成员变量追加到了__main_block_impl_0结构体中。

struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;const char *fmt;int val;
}

_ _main_block_impl_0结构体内声明的成员变量类型与自动变量类型完全相同。
请注意 Block 语法表达式中没有使用的自动变量不会被追加
初始化该结构体实例的构造函数的差异:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
//通过构造函数调用确认其参数
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

使用执行Block语法时的自动变量fmt 和 val来初始化__main_block_impl_0结构体实例。即在该源代码中,__main_block_impl_0结构体实例的初始化如下:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
fmt = "val = %d\n";
val = 10;

初始化时对fmt和val进行了赋值。由此可知,在__main_block_impl_0结构体实例中(即Block),自动变量被截获。
再看一下使用Block的匿名函数的实现:

^{printf(fmt, val)};

转换为:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {const char *fmt = __cself->fmt;int val = __cself->val;printf(fmt, val);
}

在转换后的源代码中,截获到__main_block_impl_0 结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义。因此,原来的源代码表达式无需改动便可使用截获的自动变量值执行。

在Block 中利用C语言数组类型的变量时有可能使用到的源代码。首先来看将数组传递给Block的结构体构造函数的情况。

void func(char a[10]) {printf("%d\n",a[0]);
}
int main() {char a[10] = {2};func(a);
}

该源代码可以顺利编译,并正常执行。在之后的构造函数中,将参数赋给成员变量中,这样在变换了Block语法的函数内可由成员变量赋值给自动变量。源代码预测如下。

void func(char a[10]) {char b[10] = a;printf("%d\n", b[0]);
}
int main() {char a[10] = {2};func(a);
}

该源代码将C语言数组类型变量赋值给C语言数组类型变量中,这是不能编译的。虽然变量的类型以及数组的大小都相同,但C语言规范不允许这种赋值。当然,有许多方法可以截获值,但Blocks 似乎更遵循C语言规范。

__Block说明符

Block中使用自动变量后,在Block的结构体实例中重写该自动变量也不会改变原先截获的自动变量。
以下源代码试图改变Block中的自动变量val。该代码会产生编译错误。

int val = 0;
void(^blk)(void) = ^{val = 1;};

因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。
不过这样一来就无法在Block中保存值了,极为不便。解决这个问题有两种方法。第一种:C语言中有一个变量,允许Block改写值。具体如下:

  • 静态变量
  • 静态全局变量
  • 全局变量
    静态变量的这种方法似乎也适用于自动变量的访问。但是我们为什么没有这么做呢?
    实际上,在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量。变量作用域结束的同时,原来的自动变量被废弃,因此 Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。这些在下节详细说明。
    解决Block中不能保存值这一问题的第二种方法是使用“_ block说明符”。更准确的表述方式为“ block存储域类说明符”( _block storage-class-specifier)。C语言中有以下存储域说明符:
  • typedef
  • extern
  • static
  • auto
  • register
    _block 说明符类似于 static、auto 和 register 说明符,它们用于指定将变量值设置到哪个存储域中。例如,auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。
    下面我们来实际使用
    block说明符,用它来指定Block中想变更值的自动变量。我们在前面编译错误的源代码的自动变量声明上追加 _block 说明符。
__block int val = 10;
void (^blk)(void) = ^{val = 1;};

变换后:

struct __Block_byref_val_0 {void *__isa;__Block_byref_val_0 *__forwarding;int __flags;int __size;int val;
};
struct __main_block impl_0 {struct __block_impl impl;struct __main block desc 0* Desc;__Block_byref_val_0 *val;__main_block_impl_0(void *fp, struct __main_block_desc 0 *desc, __Block_byrefval_0 *_val, int flags=0) : val(_val->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr=fp;Desc = desc;
};
static void __main_block_func_0(struct__main_block_impl_0 *_cself) {__Block_byref_val_0 *val =__cself->val;(val->__forwarding->val) = 1;
}
static void_main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}static void __main_block_dispose_0(struct __main_block_imp1_0*src) {_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}static struct __main_block_desc_0 {unsigned long reserved;unsigned long Block_size;void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);void (*dispose)(struct __main_block_impl_0*);
}__main_block_desc_0_DATA = {0,sizeof(structmain_block_impl_0),__main_block_copy_O,__main_block_dispose_0
};
int main() {__Block_byref_val_0 val = {0,&val,0,sizeof(__Block_byref_val_0),10};blk = &__mainblock_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);return 0;
}

这个__block变量val是怎样转换过来的呢?

__Block_byref_val_0 val = {0,&val,0,sizeof(_Block_byref_val_0),10
};

它竟然变为了结构体实例。__block变量也同 Block一样变成__Block_byref_val_0结构体类型的自动变量,即栈上生成的__Block_byref_val_0 结构体实例。该变量初始化为10,且这个值也出现在结构体实例的初始化中,这意味着该结构体持有相当于原自动变量的成员变量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/751876.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一. 并行处理与GPU体系架构-GPU并行处理

目录 前言0. 简述1. 这个小节会涉及到的关键字2. CPU与GPU在并行处理的优化方向3. Summary总结参考 前言 自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考 本次课程我们来学习下课程第一章——并行处理与GPU体…

Google云计算原理与应用(三)

目录 五、分布式存储系统Megastore(一)设计目标及方案选择(二)Megastore数据模型(三)Megastore中的事务及并发控制(四)Megastore基本架构(五)核心技术——复制…

pom.xml中的配置无法被yaml读取

问题描述 项目中指定了多个profiles, 但是application.yaml读取报错,报错信息如下 Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts 12:41:52.325 [mai…

数值分析复习:Newton插值

文章目录 牛顿(Newton)插值插值条件基函数插值多项式差商差商的基本性质差商估计差商的Leibniz公式 余项估计 本篇文章适合个人复习翻阅,不建议新手入门使用 牛顿(Newton)插值 插值条件 n1个插值节点 x 0 , x 1 , ……

使用 pnpm 搭建 monorepo 项目

引言 在我之前的开发经历中,并没有实际使用过 Monorepo 管理项目,尽管之前对此有所了解,但并未深入探究。然而,如今许多开源项目都采纳了 Monorepo 方式,对于不熟悉它的开发者来说,阅读和理解这些项目的源…

【HarmonyOS】ArkUI - 向左/向右滑动删除

核心知识点:List容器 -> ListItem -> swipeAction 先看效果图: 代码实现: // 任务类 class Task {static id: number 1// 任务名称name: string 任务${Task.id}// 任务状态finished: boolean false }// 统一的卡片样式 Styles func…

C语言快速入门之内存函数的使用和模拟实现

1.memcpy 它可以理解为memory copy的组合,memory有记忆的意思,这里指的是内存,copy是拷贝,这个函数是针对内存块进行拷贝的 函数原型 void* memcpy(void* destination,const void* source, size_t num); 从source位置开始&am…

ChatGPT国内镜像站大全

#今天在知乎看到一个问题:“平民不参与内测的话没有账号还有机会使用ChatGPT吗?” 从去年GPT大火到现在,关于GPT的消息铺天盖地,真要有心想要去用,途径很多,别的不说,国内GPT的镜像站到处都是&…

基于sortablejs实现拖拽element-ui el-table表格行进行排序

可以用原生的dragstart、drag、dragend、dragover、drop、dragleave实现这个效果&#xff0c;但是有现成的轮子就不要重复造了&#xff0c;看效果&#xff1a; <template><el-table :class"$options.name" :data"tableData" ref"table"…

Docker进阶教程 - 1 Dockerfile

更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 1 Dockerfile Dockerfile 是做什么的&#xff1f; 我们前面说到&#xff0c;制作镜像的方法主要有两种方式&#xff1a; 使用 docker commit 命令&#xff1b;使用 Dockerfile 文件。 但是…

leetcode每日一题310.最小高度树

目录 一.题目原型 二.题目思路 三.代码实现 一.题目原型 二.题目思路 首先&#xff0c;我们看了样例&#xff0c;发现这个树并不是二叉树&#xff0c;是多叉树。 然后&#xff0c;我们可能想到的解法是&#xff1a;根据题目的意思&#xff0c;就挨个节点遍历bfs&#xff0c;…

瑞_Redis_短信登录_Redis代替session的业务流程

文章目录 项目介绍1 短信登录1.1 项目准备1.2 基于Session实现登录流程1.3 Redis代替session的业务流程1.3.1 设计key的结构1.3.2 设计key的具体细节1.3.3 整体访问流程1.3.4 代码实现 &#x1f64a; 前言&#xff1a;本文章为瑞_系列专栏之《Redis》的实战篇的短信登录章节的R…

springboot项目读取excel表格内容到数据库,excel表格字段为整数的读取方法

在我昨天的项目中&#xff0c;我需要把excel表格中字段为整数的字段读取到数据库中进行保存&#xff0c;但是在内置方法中并没有读取整数的方法&#xff08;也有可能是我没发现&#xff0c;太菜了~~&#xff09;&#xff0c;那接下来我就提供给大家一个简单地方法来读取excel表…

Apache-Doris基础概念

OLAP数据库Doris 一、Doris架构二、基本概念1. Row & Column2. Partition & Tablet3. 建表示例&#xff08;1&#xff09;列的定义&#xff08;2&#xff09;分区分桶&#xff08;3&#xff09;多列分区&#xff08;4&#xff09;PROPERTIES&#xff08;5&#xff09;E…

【LabVIEW FPGA入门】单周期定时循环

单周期定时循环详解 单周期定时环路是FPGA编程中最强大的结构之一。单周期定时循环中的代码更加优化&#xff0c;在FPGA上占用更少的空间&#xff0c;并且比标准While循环中的相同代码执行得更快。单周期定时环路将使能链从环路中移除&#xff0c;以节省FPGA上的空间。…

windows下使用tree指定层数生成项目结构

windows自带的tree太辣鸡了&#xff0c;我们需要找东西代替 工具链接 Tree for Windows (sourceforge.net) 点击这里下载 置入Git 解压下载的压缩包&#xff0c;将bin目录下的exe复制下来 进入你的Git目录&#xff0c;将其放入Git目录下的usr/bin目录下 打开Git Bash 输入…

如何在Mac中删除照片?这里有详细步骤

前言 本文介绍如何从Mac中删除照片,以释放硬盘空间或更好地组织文件和文件夹。 如何使用废纸篓删除Mac上的图片 在Mac上删除图片的最简单方法之一是使用废纸篓功能。学习只需几秒钟。下面是如何删除单个图片以及如何在Mac上删除多个图片,以及一些关键和有用的提示,以使该…

Selenium-webdriver_manager判断是否已经下载过驱动(复用缓存驱动)

1,谷歌浏览器默认位置 2&#xff0c;ChromeDriverManager 下载的驱动位置 其中admin为机器的用户名 def installDriver(self):"""判断是否需要下载driver""""""找到本机谷歌浏览器版本""""""C:\P…

【学习心得】Python数据分析的基本思路

一、什么是数据分析&#xff1f; 数据分析是指通过一些方法&#xff0c;对一些数据进行分析&#xff0c;从中提取出有价值的信息并形成结论进行展示。 &#xff08;1&#xff09;一些方法 数学和统计学方法&#xff1a;例如回归分析、聚类分析、主成分分析、时间序列分析等&a…

【代码随想录算法训练营第二十五天 | LeetCode216.组合总和III、17.电话号码的字母组合】

代码随想录算法训练营第二十五天 | LeetCode216.组合总和III、17.电话号码的字母组合 一、77. 组合 解题代码C&#xff1a; class Solution { private:vector<vector<int>> result; // 存放结果集vector<int> path; // 符合条件的结果void backtracking(in…