Objective-C 之 Block 详解

作者:唐诺日期:2026/7/7

iOS核心技能:Objective-C Block完全详解

从语法到原理,从内存到选型,一篇打通Block所有知识点

Block是Objective-C最核心的特性之一,也是每个iOS开发者必须掌握的知识点。它既是回调利器,也是内存陷阱的高发区。本文将系统讲解Block的一切,并在最后与Method进行全方位对比,帮助你彻底掌握Block的选型与使用。


一、什么是Block?

Block是Objective-C对C语言函数指针的扩展,它本质上是一个带有自动变量值的匿名函数对象。简单理解:Block = 函数体 + 捕获的上下文变量。

1// 一个最简单的Block
2^{
3    NSLog(@"Hello Block");
4};
5

1.1 闭包的本质

Block是一种闭包(Closure)实现。闭包是指一个函数可以捕获并保存其所在作用域中的变量,即使该作用域已经销毁,这些变量依然可以被访问和修改。

1- (void (^)(void))createCounter {
2    __block int count = 0;
3    void (^counter)(void) = ^{
4        count++;
5        NSLog(@"count = %d", count);
6    };
7    return counter;
8}
9
10// 外部调用
11void (^counter)(void) = [self createCounter];
12counter(); // count = 1
13counter(); // count = 2
14// count变量被Block持久化保存了
15

二、Block的语法详解

2.1 基本语法

1// 1. 最简单的Block
2^{
3    NSLog(@"无参数无返回值");
4};
5
6// 2. 带参数的Block
7^(int a, int b) {
8    return a + b;
9};
10
11// 3. 带返回值类型的完整写法
12^int (int a, int b) {
13    return a + b;
14};
15

2.2 声明与赋值

1// 无参数无返回值
2void (^simpleBlock)(void) = ^{
3    NSLog(@"简单Block");
4};
5
6// 带参数和返回值
7int (^addBlock)(int, int) = ^(int a, int b) {
8    return a + b;
9};
10
11// 使用
12simpleBlock();
13int result = addBlock(3, 5); // result = 8
14

2.3 使用typedef简化

1// 定义Block类型
2typedef void (^CompletionBlock)(BOOL success, NSError *error);
3typedef int (^CalculatorBlock)(int a, int b);
4
5// 使用类型
6CompletionBlock completion = ^(BOOL success, NSError *error) {
7    if (success) {
8        NSLog(@"成功");
9    } else {
10        NSLog(@"失败: %@", error);
11    }
12};
13
14CalculatorBlock add = ^(int a, int b) {
15    return a + b;
16};
17
18CalculatorBlock multiply = ^(int a, int b) {
19    return a * b;
20};
21

2.4 Block作为参数

1// 定义方法,接受Block作为参数
2- (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion {
3    // 异步操作
4    dispatch_async(dispatch_get_global_queue(0, 0), ^{
5        NSData *data = [self loadData];
6        dispatch_async(dispatch_get_main_queue(), ^{
7            if (completion) {
8                completion(data, nil);
9            }
10        });
11    });
12}
13
14// 调用
15[self fetchDataWithCompletion:^(NSData *data, NSError *error) {
16    if (error) {
17        NSLog(@"加载失败");
18    } else {
19        NSLog(@"加载成功,数据大小: %lu", data.length);
20    }
21}];
22
方法结构拆解
1- (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion {
2    // 这里是方法的实现
3}
4
  • -:表示这是一个实例方法,只能通过类的实例来调用。
  • (void):表示这个方法没有返回值
  • fetchDataWithCompletion::是方法名。通常用于发起一个异步操作(比如网络请求)。
  • (void (^)(NSData *data, NSError *error))completion:这是方法唯一的参数,比较长,咱们拆开看:

2.5 Block作为属性

1@interface MyClass : NSObject
2@property (nonatomic, copy) void (^dataLoadedBlock)(NSData *data);
3@property (nonatomic, copy) CompletionBlock completionHandler;
4@end
5
6// 使用
7MyClass *obj = [[MyClass alloc] init];
8obj.dataLoadedBlock = ^(NSData *data) {
9    NSLog(@"数据已加载: %lu", data.length);
10};
11

三、Block的底层原理

3.1 Block的数据结构

Block在底层是一个结构体对象,其简化结构如下:

1struct Block_layout {
2    void *isa;                      // 指向类对象(三种Block类型)
3    int flags;                      // 标志位
4    int reserved;                   // 保留字段
5    void (*invoke)(void *, ...);    // 函数指针,指向Block执行代码
6    struct Block_descriptor *descriptor; // 描述信息(大小、copy/dispose辅助函数)
7    // 捕获的变量...
8};
9

3.2 Block的三种类型

Block通过isa指针指向不同的类,分为三种类型:

Block类型isa指向存储位置产生条件
**NSGlobalBlock_NSConcreteGlobalBlock全局数据区不捕获外部变量,或捕获全局/静态变量
**NSStackBlock_NSConcreteStackBlock栈区捕获局部变量,在栈上创建
**NSMallocBlock_NSConcreteMallocBlock堆区从栈Block拷贝而来(ARC自动)
1// 全局Block - 不捕获外部变量
2void (^globalBlock)(void) = ^{
3    NSLog(@"Global Block");
4};
5
6// 栈Block - 捕获局部变量(ARC下可能被优化到堆上)
7int a = 10;
8void (^stackBlock)(void) = ^{
9    NSLog(@"%d", a);
10};
11
12// 堆Block - 显式拷贝
13void (^heapBlock)(void) = [stackBlock copy];
14

3.3 Block捕获变量机制

1int a = 10;              // 局部变量
2__block int b = 20;      // __block修饰
3static int c = 30;       // 静态变量
4NSString *str = @"Hello"; // 对象
5
6void (^block)(void) = ^{
7    NSLog(@"a = %d", a);       // 值捕获  a被复制为const,不可修改
8    NSLog(@"b = %d", b);       // 引用捕获  b被包装为结构体指针
9    NSLog(@"c = %d", c);       // 指针捕获  直接操作原变量
10    NSLog(@"str = %@", str);   // 对象引用  强引用str
11};
12

捕获规则总结:

变量类型捕获方式是否可修改说明
局部变量(基本类型)值捕获复制为常量
局部变量(对象类型)强引用捕获⚠️可修改对象内容,不可重新赋值
静态变量指针捕获直接操作原变量
全局变量直接访问不捕获,直接访问
__block变量引用捕获(结构体指针)通过结构体间接访问

3.4 拷贝操作详解

1// 栈Block  堆Block(手动拷贝)
2void (^stackBlock)(void) = ^{
3    NSLog(@"Stack Block");
4};
5void (^heapBlock)(void) = [stackBlock copy];
6
7// ARC下自动拷贝的场景:
8// 1. 将Block赋值给__strong指针(大多数情况)
9// 2. Block作为返回值返回
10// 3. 将Block赋值给copy修饰的属性
11// 4. 将Block作为参数传递时(某些情况需要手动copy)
12
13// 为什么属性要用copy修饰?
14@property (nonatomic, copy) void (^myBlock)(void);
15// 确保Block被拷贝到堆上,防止栈Block提前销毁
16

四、Block的内存管理

4.1 循环引用问题

Block会强引用捕获的对象,这是循环引用的根本原因。

1//  经典循环引用
2@interface ViewController ()
3@property (nonatomic, copy) void (^completionBlock)(void);
4@end
5
6@implementation ViewController
7- (void)setupBlock {
8    self.completionBlock = ^{
9        // self  completionBlock  self(循环引用)
10        [self doSomething];
11    };
12}
13@end
14

4.2 打破循环引用的方法

方法一:weak-strong dance(最常用)

1__weak typeof(self) weakSelf = self;
2self.completionBlock = ^{
3    __strong typeof(weakSelf) strongSelf = weakSelf;
4    if (strongSelf) {
5        [strongSelf doSomething];
6        [strongSelf updateUI];
7    }
8};
9

方法二:使用@weakify/@strongify(ReactiveCocoa)

1@weakify(self);
2self.completionBlock = ^{
3    @strongify(self);
4    [self doSomething];
5};
6

方法三:传入self作为参数

1self.completionBlock = ^(ViewController *vc) {
2    [vc doSomething];
3};
4// 调用时
5self.completionBlock(self);
6

方法四:使用__block并手动置nil

1__block ViewController *blockSelf = self;
2self.completionBlock = ^{
3    [blockSelf doSomething];
4    blockSelf = nil; // 必须置nil
5};
6// 注意:这种方式需要保证Block一定会被执行
7

4.3 __block的内存管理

1// __block变量在Block中的内存管理
2__block int count = 0;  // 在栈上
3void (^block)(void) = ^{
4    count++; // Block拷贝时,__block变量也会被拷贝到堆上
5};
6
7// ARC下:
8// __block变量被包装成一个结构体对象
9// 结构体中有__forwarding指针,保证栈和堆上的变量一致性
10

五、Block的常见使用场景

5.1 异步回调(最核心场景)

1// 网络请求回调
2- (void)downloadImageWithURL:(NSURL *)url
3                  completion:(void (^)(UIImage *image, NSError *error))completion {
4    NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:url
5        completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
6            dispatch_async(dispatch_get_main_queue(), ^{
7                if (error) {
8                    completion(nil, error);
9                } else {
10                    UIImage *image = [UIImage imageWithData:data];
11                    completion(image, nil);
12                }
13            });
14        }];
15    [task resume];
16}
17
18// 动画回调
19[UIView animateWithDuration:0.3 animations:^{
20    self.view.alpha = 0;
21} completion:^(BOOL finished) {
22    [self.view removeFromSuperview];
23}];
24

5.2 遍历操作

1// 数组遍历
2NSArray *array = @[@1, @2, @3, @4, @5];
3[array enumerateObjectsUsingBlock:^(NSNumber *num, NSUInteger idx, BOOL *stop) {
4    if (num.integerValue > 3) {
5        *stop = YES; // 提前终止
6    }
7    NSLog(@"%@", num);
8}];
9
10// 字典遍历
11NSDictionary *dict = @{@"key1": @"value1", @"key2": @"value2"};
12[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
13    NSLog(@"%@ : %@", key, obj);
14}];
15
16// 并发遍历(高性能)
17NSArray *largeArray = ...;
18[largeArray enumerateObjectsWithOptions:NSEnumerationConcurrent
19                             usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
20    // 并发处理,适合大数据量
21}];
22

5.3 排序与筛选

1// 排序
2NSArray *sorted = [array sortedArrayUsingComparator:^NSComparisonResult(NSString *s1, NSString *s2) {
3    return [s1 compare:s2];
4}];
5
6// 自定义排序
7NSArray *people = @[...];
8NSArray *sortedPeople = [people sortedArrayUsingComparator:^NSComparisonResult(Person *p1, Person *p2) {
9    return [p1.age compare:p2.age];
10}];
11

5.4 链式调用(DSL)

1// 实现链式调用(参考Masonry)
2@interface Calculator : NSObject
3@property (nonatomic, assign) int result;
4- (Calculator *(^)(int))add;
5- (Calculator *(^)(int))subtract;
6- (Calculator *(^)(int))multiply;
7- (Calculator *(^)(int))divide;
8@end
9
10@implementation Calculator
11- (Calculator *(^)(int))add {
12    return ^Calculator *(int value) {
13        self.result += value;
14        return self;
15    };
16}
17- (Calculator *(^)(int))subtract {
18    return ^Calculator *(int value) {
19        self.result -= value;
20        return self;
21    };
22}
23// ... 其他方法
24@end
25
26// 使用:流畅的链式调用
27Calculator *calc = [[Calculator alloc] init];
28int result = calc.add(5).subtract(3).multiply(4).result; // 8
29

5.5 GCD多线程

1// 异步执行
2dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
3    // 耗时操作
4    id result = [self heavyTask];
5    dispatch_async(dispatch_get_main_queue(), ^{
6        // 更新UI
7        [self updateUI:result];
8    });
9});
10
11// 延迟执行
12dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),
13               dispatch_get_main_queue(), ^{
14    NSLog(@"2秒后执行");
15});
16
17// 组队列
18dispatch_group_t group = dispatch_group_create();
19dispatch_group_async(group, queue, ^{ /* 任务1 */ });
20dispatch_group_async(group, queue, ^{ /* 任务2 */ });
21dispatch_group_notify(group, mainQueue, ^{ /* 所有任务完成 */ });
22

六、Block vs Method:全方位对比

6.1 核心区别一览

对比维度BlockMethod
本质匿名函数对象(结构体)类的成员函数(SEL+IMP)
有名称❌ 匿名(可赋给变量)✅ 有方法名
所属关系独立对象属于类或实例
定义位置任意位置(函数内、方法内、全局)必须在@interface/实现中
捕获局部变量✅ 可以捕获❌ 只能访问自身成员/全局变量
修改外部变量⚠️ 需__block修饰⚠️ 修改自身实例变量直接操作
作为参数传递✅ 直接传递❌ 需通过SEL+performSelector
作为返回值✅ 可以❌ 不可以
存储类型全局/栈/堆三种随对象存储
内存管理需要关注循环引用较少关注(对象生命周期管理)
调用方式block() 函数指针调用[obj method] 消息发送
调用效率较高(直接调用)略低(消息转发开销)
动态性较低(编译时确定)较高(运行时决议)
继承与重写❌ 不支持✅ 支持

6.2 变量访问能力对比

1// 方法中的变量访问
2- (void)methodExample {
3    int localVar = 10;           // 局部变量
4    NSString *str = @"Hello";    // 局部变量
5    
6    //  方法无法直接使用局部变量(除非作为参数)
7    //  可以访问实例变量
8    self.instanceVar = @"value";
9    //  可以访问全局变量
10    globalVar = 100;
11    //  可以访问静态变量
12    staticVar = 200;
13}
14
15// Block中的变量访问
16- (void)blockExample {
17    int localVar = 10;           // 局部变量
18    __block int mutableVar = 20; // 可修改的局部变量
19    NSString *str = @"Hello";    // 局部对象
20    
21    void (^block)(void) = ^{
22        //  可以捕获并访问局部变量
23        NSLog(@"%d", localVar);
24        //  可以修改__block变量
25        mutableVar++;
26        //  可以访问实例变量
27        self.instanceVar = @"value";
28        //  可以修改捕获的对象内容
29        // str = @"World"; //  不可重新赋值,但可修改对象内容
30    };
31}
32

6.3 内存管理对比

方法的内存管理:

1// 方法不产生独立的循环引用问题
2@interface MyClass : NSObject
3@property (nonatomic, strong) NSString *name;
4@end
5
6- (void)doSomething {
7    //  调用自身方法,不会产生循环引用
8    [self helperMethod];
9    //  访问自身属性,不会循环引用
10    self.name = @"value";
11}
12

Block的内存管理:

1// Block容易造成循环引用
2@interface MyClass : NSObject
3@property (nonatomic, copy) void (^block)(void);
4@end
5
6//  循环引用
7- (void)setupBlock {
8    self.block = ^{
9        [self doSomething]; // self  block  self
10    };
11}
12
13//  正确做法
14- (void)setupBlock {
15    __weak typeof(self) weakSelf = self;
16    self.block = ^{
17        __strong typeof(weakSelf) strongSelf = weakSelf;
18        [strongSelf doSomething];
19    };
20}
21

6.4 性能对比

维度BlockMethod
调用方式函数指针直接调用objc_msgSend消息发送
运行时开销极小(内存间接寻址)较大(动态查找+转发)
是否可内联简单Block可内联优化不支持内联
内存占用额外结构体内存(捕获变量)无额外内存
适用场景回调、遍历、函数式对象行为、继承多态

6.5 使用场景决策树

1需要定义对象的核心行为/职责?
2     使用方法
3    ├── 需要被继承和重写?  方法(优先)
4    ├── 需要作为公开API?  方法
5    └── 需要访问私有成员?  方法(天然支持)
6
7需要作为参数传递或返回值?
8     使用Block
9    ├── 异步回调、完成回调?  Block
10    ├── 遍历操作的处理逻辑?  Block
11    ├── 需要捕获上下文状态?  Block
12    ├── 一次性的临时逻辑?  Block
13    └── 链式调用/DSL构建?  Block
14

6.6 具体场景推荐

场景推荐方案理由
网络请求回调Block代码紧凑,上下文自然
按钮点击事件Method(Target-Action)传统方式,容易调试
数组遍历BlockAPI原生支持,简洁
对象核心方法Method符合OOP设计
多线程任务BlockGCD API原生使用Block
动画完成回调BlockUIKit API标准
排序比较逻辑Block灵活,可内联定义
代理方法Method(Delegate)适合复杂交互
依赖注入Block轻量级解耦
算法封装Method便于测试和复用

七、常见陷阱与注意事项

7.1 在Block中使用self

1//  错误:可能导致循环引用
2self.block = ^{
3    [self update];
4};
5
6//  正确:使用weakSelf
7__weak typeof(self) weakSelf = self;
8self.block = ^{
9    [weakSelf update];
10};
11
12// ⚠️ 注意:如果Block中需要连续多次使用self
13__weak typeof(self) weakSelf = self;
14self.block = ^{
15    // 建议在Block开始时转为strong,防止执行过程中对象被释放
16    __strong typeof(weakSelf) strongSelf = weakSelf;
17    if (strongSelf) {
18        [strongSelf step1];
19        [strongSelf step2];
20        [strongSelf step3];
21    }
22};
23

7.2 Block属性必须用copy

1//  错误:使用strong可能指向栈Block
2@property (nonatomic, strong) void (^block)(void);
3
4//  正确:使用copy确保在堆上
5@property (nonatomic, copy) void (^block)(void);
6

7.3 在Block中修改外部变量

1//  错误:无法修改
2int count = 0;
3void (^block)(void) = ^{
4    count++; // 编译错误
5};
6
7//  正确:使用__block
8__block int count = 0;
9void (^block)(void) = ^{
10    count++;
11};
12

7.4 通知中心移除问题

1// 注意:使用Block添加通知时,记得移除观察者
2__weak typeof(self) weakSelf = self;
3self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"Notification"
4    object:nil queue:nil usingBlock:^(NSNotification *note) {
5        [weakSelf handleNotification];
6    }];
7
8// 记得在dealloc中移除
9- (void)dealloc {
10    [[NSNotificationCenter defaultCenter] removeObserver:self.observer];
11}
12

八、常见面试题

Q1: Block的底层原理是什么?

Block底层是一个结构体对象,包含isa指针、flags、invoke函数指针和descriptor。invoke指向Block的执行代码,通过直接调用函数指针执行,而不是消息发送。

Q2: Block为什么用copy修饰?

Block默认在栈上,栈Block在作用域结束后会被销毁。使用copy将Block拷贝到堆上,由ARC管理生命周期,确保在外部使用时依然有效。

Q3: __block和__weak的区别?

__block让Block可以修改外部变量,在ARC下将变量包装为结构体指针;__weak是弱引用,用于打破循环引用,但不能修改变量值。

Q4: Block的循环引用如何解决?

使用weak-strong dance:先用__weak弱引用self,Block内部再用__strong转为强引用。或者使用__block并手动置nil,或使用参数传递方式。

Q5: 为什么Block捕获的对象会被强引用?

Block在捕获对象时会调用_Block_object_assign函数,默认对对象进行强引用(retain),以确保Block执行时对象仍然存在。

Q6: Block和方法的区别是什么?

  • Block是匿名函数对象,可捕获变量,可传递
  • 方法是类的成员函数,不能捕获局部变量,不可传递
  • Block调用是函数指针直接调用,方法是消息发送
  • Block有循环引用风险,方法基本没有

九、最佳实践总结

9.1 编码规范

  1. 使用typedef定义Block类型,提高代码可读性
  2. Block属性使用copy修饰
  3. 调用Block前检查是否为nil
  4. Block内部使用weak-strong dance避免循环引用
  5. 合理使用__block修饰需要修改的变量

9.2 内存管理规范

场景推荐做法
Block作为属性@property (nonatomic, copy)
Block中访问self__weak typeof(self) weakSelf = self
Block中修改外部变量使用__block修饰
嵌套Block每层都需要处理循环引用
大对象捕获考虑使用weak或手动管理

9.3 选型决策

1使用Method的场景:
2├── 对象的核心行为和职责
3├── 需要被继承和重写
4├── 公开API接口
5└── 复杂业务逻辑
6
7使用Block的场景:
8├── 异步回调/完成处理器
9├── 遍历操作的处理逻辑
10├── 排序/比较/筛选逻辑
11├── 延迟执行/定时任务
12├── 链式调用/DSL
13└── 一次性的临时逻辑
14

十、总结

Block是Objective-C中最具表现力的特性之一,它将闭包编程带入了iOS开发。理解Block不仅需要掌握语法,更要深入其内存管理和底层原理。

核心要点回顾:

  1. 本质:Block是匿名函数对象,可以捕获上下文变量
  2. 类型:全局Block、栈Block、堆Block,ARC下自动管理
  3. 内存:注意循环引用,使用weak-strong dance打破
  4. 应用:异步回调、遍历、排序、链式调用、GCD
  5. 对比方法:Block更灵活可传递,方法更符合OOP设计

Block和方法是互补关系,而非替代关系。优秀的代码往往是"方法定义结构,Block实现流程"的有机结合。


Objective-C 之 Block 详解》 是转载文章,点击查看原文


相关推荐


用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
小林的编程开发日记2026/6/29

用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台 大家好,去年我写过一篇 Flutter 全链路监控 SDK 那一版可以理解成 v1。 v1 的目标很直接:Flutter App 里发生了什么问题,我要尽可能采到。 比如: Flutter framework error; Dart uncaught error; 启动耗时; 页面加载耗时; Dio / http 请求耗时和状态码; 页面 PV 和停留时长; 用户关键点击; Flutter fram


Python 五大核心数据结构完全指南:列表(List)、字符串(String)、元组(Tuple)、集合(Set)、字典(Dictionary)
FrontPy2026/6/20

Python 核心数据结构完全指南 一、列表 (List) 1.1 定义与特点 列表是 Python 中最常用的数据容器,可以一次性存储多个元素。 定义: 列表名 = [元素1, 元素2, 元素3, ...] 例如: s = [54, 15, 75, 108, 23, 78, 75] 特点: 可以存储不同类型的元素(数字、字符串、布尔值等混合存放)。元素有序,排列顺序固定。元素可以重复。元素可以被修改(增、删、改)。属于序列类型:可通过索引访问。 1.2 列表的索引 正向索引:从 0 开始,


prototype 注入到 singleton 里,prototype是否还是线程安全的
swordbob2026/6/12

"如果 prototype Bean 内部没有共享状态,自身是线程安全的。但 Spring 容器对 prototype 的多例行为有'陷阱',可能导致实际使用中不安全。" 展开讲: prototype 注入到 singleton 里的线程安全分析 一、prototype Bean 自身的线程安全性 1.1 prototype 本身是"独立实例"(这个是安全的) @Service@Scope("prototype")public class ShoppingCart { priv


局部两极分析破解数学建模难题
luj_17682026/6/5

当前博文未提及该问题,以下是基于C知道的知识内容为您回答 在数学建模中,通过“局部两极分析”实现问题求解的突破,其核心思想是:在复杂系统的局部或特定维度上,分别考察其极大化与极小化两种极端状态或趋势,通过分析这两种对立极值的行为、约束与相互作用,来洞察系统的本质结构、关键矛盾与优化路径,从而为全局问题的求解找到突破口。 这种方法融合了优化理论、博弈论和系统分析的思想。 一、 “局部两极分析”的核心逻辑与步骤 “局部两极分析”并非一个固定的算法,而是一种建模策略与思维框架。其通用实施流程可概


ASP.NET Core Razor 语法简述
叫我少年2026/5/30

如果写过 HTML 里嵌 <?php ?>​ 的页面吧?Razor 干的事差不多,只不过嵌入式用的是 C#。Razor 标记可以塞进 .cshtml​(MVC 视图/Razor Pages)或者 .razor​(Blazor 组件),写法跟 Vue 的模板语法有几分神似。下面从最基础的开始,一路讲到那些容易被忽视的坑。 1. 怎么输出 HTML Razor 的默认语言就是 HTML,你写 <p>Hello</p>​,它就乖乖输出 <p>Hello</p>​。但是想混入 C# 逻辑的时候,就得靠


Layui结合springboot读取返回值,前端展示简单示例
一个心烑2026/5/7

一、SpringBoot 后端接口 + Layui 前端页面实现 1 后端返回 JSON 数据 2 前端 Layui 表格展示数据 3 前端表单提交保存到后端 4 完整的异步请求(Ajax)处理 项目结构(标准 SpringBoot): springboot-layui-demo/ ├── src/main/java/com/demo/ │ ├── controller/ │ │ └── UserController.java # 接口控制器 │ ├── entity/ │


算个账也要开顶配 AI?我让 AI 自己劝我换了个小的
小墨同学boy2026/4/28

其实我自己每个月在 AI 上花的钱不少,Claude、GPT、GLM 都有订阅。但其实有很大一部分预算其实是花在处理生活琐事上的——记账、外卖归类、做一个消费数据分析。这种活差不多每天都得跑一遍,所以token 烧得也不少,尤其是有时候开了深度思考的模型反而更容易给错数据。 后来我才反应过来,手里订阅一个不少,结果我一直在拿 Opus 的钱让它当我的计算器。有点奢侈了,所以我准备切换思路看看有没有节省token的办法。 一、我发现一个反直觉的事 在日常生活当中,我的直觉一直是有钱就上好的贵的效果


2026 年前端工程师面试:一份来自面试官视角的真实复盘
怕浪猫2026/4/19

前言:为什么我要写这篇文章 前两天和一个在高校和企业都面试过不少候选人的"面试官老炮"聊天,他听过太多候选人抱怨面试内容脱离实际、工作用不到。也听过面试官抱怨候选人只会背题、动手能力差。有意思的是,这两拨人的抱怨,往往都对。 今天我想换个视角——不站在候选人角度刷题,也不站在理论派角度讲八股文,而是站在有实际招聘需求、真正要带团队干活的面试官视角,聊聊 2026 年的前端工程师面试,到底在考什么、为什么这么考。 核心结论先行 2026 年的前端面试,考察维度已经发生了结构性变化:


【折腾】windows下小狼毫输入法更新字库
stereohomology2026/4/11

我问DeepSeek如何更新,而不是自己从文档里面找,结果效率很高、回答很全面,直接给出步骤和命令。 作为菜鸟,我用第一种方法: 这个错误提示很明确:你当前所在的目录(即小狼毫的用户文件夹)不是空的,而 git clone 要求目标目录必须是空目录才能执行。 小狼毫默认安装后,用户文件夹里会自带一些基础配置文件(比如 default.custom.yaml、weasel.custom.yaml、luna_pinyin.schema.yaml 等)。直接 git clone 到非空目录就会报这个


吴恩达《LangChain LLM 应用开发精读笔记》8-Document Loading 文档加载
勇气要爆发2026/4/3

大家好,我是飞哥!👋 欢迎来到吴恩达《LangChain:Chat with Your Data》系列课程的第二讲。上一讲我们了解了 RAG 的全流程,今天我们正式开工,攻克第一关:Document Loading (文档加载)。 1. 为什么:Garbage In, Garbage Out 🗑️ 💡 场景锚定 做饭要先买菜洗菜,训练 AI 也是一样。如果你喂给 AI 的数据格式乱七八糟、乱码满天飞,那么 AI 输出的答案肯定也是一塌糊涂。 PDF 里有复杂的表格和页眉页脚。网页里有一堆

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读