C++20新特性:Range库

之前我们已经学习过C++20的概念与约束。今天我们就来学习继承了概念与约束的标准库新的功能库Range库

相关代码上传至gitee:

官方文档:Standard library header <ranges> (C++20) - cppreference.com

目录

Range库介绍

前言

包含内容

[1. 核心概念(Concepts)------ 类型系统的基石](#1. 核心概念(Concepts)—— 类型系统的基石)

[2. 算法(Algorithms)------ std::ranges:: 命名空间](#2. 算法(Algorithms)—— std::ranges:: 命名空间)

[3. Range 适配器(Range Adaptors)------ 惰性管道](#3. Range 适配器(Range Adaptors)—— 惰性管道)

[4. 工厂(Factories / Range Factories)](#4. 工厂(Factories / Range Factories))

优势

消灭迭代器对------代码量减半

管道组合------从命令式到声明式

惰性求值------零开销抽象

更丰富的返回值

[投影(Projection)------无需 Lambda 包装器](#投影(Projection)——无需 Lambda 包装器)

[Concepts 约束------编译期安全带](#Concepts 约束——编译期安全带)

[对内置数组和 std::span 的原生支持](#对内置数组和 std::span 的原生支持)

Range视图

视图的核心特点与优势

适配器视图

工厂视图

综合实践


Range库介绍

前言

Ranges 是对 STL 算法与迭代器体系的一次根本性重构。 传统 STL 算法用一对迭代器 [begin, end) 表示操作范围,而 Ranges 将这个"范围"抽象为一等公民------一个可以直接传递的对象

核心变化:

cpp 复制代码
// C++17 传统写法:必须传两个迭代器
std::sort(vec.begin(), vec.end());

// C++20 Ranges 写法:传一个 range 对象即可
std::ranges::sort(vec);

这个看似微小的语法变化背后,是整个泛型编程范式的升级。

包含内容

整个库基于 <ranges> 头文件,分为四大层次:

1. 核心概念(Concepts)------ 类型系统的基石

Concept 含义
std::ranges::range 任何有 begin()end() 的类型
std::ranges::sized_range 可以在常数时间获取大小的 range
std::ranges::view 轻量级、O(1) 拷贝/移动的 range
std::ranges::input_range 至少支持输入迭代器的 range
std::ranges::forward_range 支持前向迭代器
std::ranges::bidirectional_range 支持双向迭代器
std::ranges::random_access_range 支持随机访问迭代器
std::ranges::contiguous_range 内存连续布局(如 vector, array, span
std::ranges::common_range begin()end() 返回相同类型的 range

2. 算法(Algorithms)------ std::ranges:: 命名空间

所有传统 STL 算法的 Range 版本 ,约束在 std::ranges 命名空间下。与传统算法有两个关键区别:

  • 第一个参数是 range 对象,而非迭代器对

  • 返回类型更丰富:不再只返回一个迭代器,而是返回包含更多信息的结构化结果

    // 传统 STL:返回迭代器,要找插入位置还得再算
    auto it = std::find(vec.begin(), vec.end(), 42);

    // C++20 Ranges:接受 range,返回迭代器
    auto it = std::ranges::find(vec, 42);

3. Range 适配器(Range Adaptors)------ 惰性管道

这是 Ranges 库最具革命性 的部分。适配器是惰性求值的视图转换,通过 | 管道运算符串联,构成声明式的数据处理流水线:

适配器 作用
std::views::filter(pred) 按谓词过滤元素
std::views::transform(fn) 对每个元素应用函数
std::views::take(n) 取前 n 个元素
std::views::drop(n) 跳过前 n 个元素
std::views::take_while(pred) 取满足条件的连续前缀
std::views::drop_while(pred) 丢弃满足条件的连续前缀
std::views::reverse 反转范围
std::views::join 展平嵌套 range
std::views::split(delim) 按分隔符切分
std::views::common 转换为 common_range
std::views::keys / values 提取 pair/tuple 的 key 或 value
std::views::iota 生成递增序列
std::views::all 将 range 包装为 view

4. 工厂(Factories / Range Factories)

不需要输入 range,直接生成 range 的工具:

  • std::views::iota(start, end) --- 生成 [start, end) 整数序列
  • std::views::empty<T> --- 空 range
  • std::views::single(x) --- 只包含单一元素的 range
  • std::views::istream<T>(stream) --- 从输入流读取

优势

消灭迭代器对------代码量减半

cpp 复制代码
// C++17:写两遍容器名
std::sort(v.begin(), v.end());
auto it = std::find_if(v.begin(), v.end(), pred);

// C++20:容器名只出现一次
std::ranges::sort(v);
auto it = std::ranges::find_if(v, pred);

这不是语法糖------它消除了迭代器不匹配的可能性,从类型系统层面杜绝了 sort(v1.begin(), v2.end()) 这类经典 bug。

管道组合------从命令式到声明式

这是最重要的心智模型转变。传统写法是命令式 的:你需要一步步告诉 CPU 要做什么。管道写法是声明式的:你描述数据应该经过怎样的变换。

cpp 复制代码
// C++17 命令式:需要中间容器,多次遍历
std::vector<int> even;
std::copy_if(v.begin(), v.end(), std::back_inserter(even),
             [](int x) { return x % 2 == 0; });
std::vector<int> squared;
std::transform(even.begin(), even.end(), std::back_inserter(squared),
               [](int x) { return x * x; });
auto it = std::find_if(squared.begin(), squared.end(),
                       [](int x) { return x > 100; });

// C++20 声明式:一次遍历,零拷贝,惰性求值
auto result = v
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::drop_while([](int x) { return x <= 100; });

for (auto x : result) { /* 只在遍历时才真正计算 */ }

惰性求值------零开销抽象

管道中的每个适配器在定义时不执行任何计算 。计算被推迟到消费时才发生,且所有变换融合成单次遍历。没有中间分配,没有额外拷贝。

更丰富的返回值

传统 std::find 只返回一个迭代器,还需要重新计算到开头的距离等操作。Range 算法返回结构化的结果对象:

cpp 复制代码
// C++17
auto it = std::find(v.begin(), v.end(), 42);
auto pos = std::distance(v.begin(), it); // 又遍历一次

// C++20:直接通过结构化绑定获取所需信息
struct find_result { iterator found; bool success; }; // 伪代码示例
// 实际例子------ranges::sort 返回 borrowed_iterator_t<R>

投影(Projection)------无需 Lambda 包装器

每个 Range 算法都支持一个 Projection 参数,可以在比较/操作之前对元素做变换,而无需手动写 lambda:

cpp 复制代码
// C++17:lambda 包装比较
std::sort(v.begin(), v.end(),
          [](const Person& a, const Person& b) { return a.age < b.age; });

// C++20:投影 + 默认比较
std::ranges::sort(v, std::less{}, &Person::age);
//                    ^^^^^^^^^  ^^^^^^^^^^^^
//                    比较函数      投影(要对什么排序)

Concepts 约束------编译期安全带

Range 算法通过 Concept 对其输入做了严格约束。如果一个类型不满足 input_range,调用 ranges::find 在编译期就会给出清晰的错误信息,而不是在实例化后抛出几十行的模板错误。

复制代码
int x = 42;
// std::ranges::sort(x);  // 编译错误,清晰提示 int 不是 range

对内置数组和 std::span 的原生支持

cpp 复制代码
int arr[] = {3, 1, 4, 1, 5};
std::ranges::sort(arr);  // 直接对 C 数组排序,不需要 std::begin/std::end

Range视图

视图的核心特点与优势

视图 (View) 是 Ranges 库的核心抽象,具有以下关键特征:

  1. O(1) 拷贝/移动 --- 视图不拥有数据,只持有底层 range 的引用或轻量级状态
  2. 惰性求值 --- 定义管道时不执行任何计算,消费时才按需计算
  3. 融合遍历 --- 多个视图通过 | 串联后,编译器将其融合为等效的单次手写循环
  4. 不修改原数据 --- 视图是"数据的观察窗口",变换操作生成新视图而不修改底层容器
  5. 编译期类型安全 --- 基于 Concepts 约束,视图间的非法连接会在编译期被拦截

适配器视图

需要输入 range,对数据做变换

适配器 功能 典型场景
filter(pred) 保留满足谓词的元素 筛选偶数、有效记录
transform(fn) 对每个元素应用映射函数 类型转换、提取字段
take(n) 取前 n 个元素后停止 分页、TOP-N 查询
drop(n) 跳过前 n 个元素 跳过表头、去除前导
take_while(pred) 取满足条件的连续前缀 读取到终止标记
drop_while(pred) 丢弃满足条件的连续前缀 去除前导空白
reverse 反转 range(需 bidirectional) 逆序遍历
split(delim) 按分隔符切割(range of ranges) 字符串分词
join 展平嵌套 range 二维转一维
keys 提取 pair/tuple 的第一个元素 遍历 map 的 key
values 提取 pair/tuple 的第二个元素 遍历 map 的 value
all 将 range 包装为 view 显式开启管道
common 统一 begin() 和 end() 返回类型 适配遗留接口

代码示例:

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <map>
#include <list>
#include <sstream>

namespace r = std::ranges;
namespace v = std::views;

template<typename R>
void print_range(std::string_view name, R&& r) {
    std::cout << name << ": ";
    for (auto&& e : r) std::cout << e << ' ';
    std::cout << '\n';
}

// ----------------------------------------------------------------------------
int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // ================================================================
    // 1. filter  ------ 按谓词过滤,保留满足条件的元素
    // ================================================================
    {
        auto even = data | v::filter([](int x) { return x % 2 == 0; });
        print_range("filter(偶数)", even);  // 2 4 6 8 10
    }

    // ================================================================
    // 2. transform ------ 对每个元素施加映射函数
    // ================================================================
    {
        auto squared = data | v::transform([](int x) { return x * x; });
        print_range("transform(平方)", squared);  // 1 4 9 16 25 36 49 64 81 100
    }

    // ================================================================
    // 3. 管道组合:filter + transform ------ 惰性求值,单次遍历
    // ================================================================
    {
        auto pipeline = data
            | v::filter([](int x) { return x % 2 == 0; })       // 先过滤偶数
            | v::transform([](int x) { return x * x; });        // 再平方
        print_range("filter + transform(偶数的平方)", pipeline); // 4 16 36 64 100
    }

    // ================================================================
    // 4. take(n) ------ 取前 n 个元素,达到 n 个后立即停止遍历
    //    drop(n) ------ 跳过前 n 个元素
    // ================================================================
    {
        auto first3 = data | v::take(3);
        print_range("take(3)", first3);  // 1 2 3

        auto skip3 = data | v::drop(3);
        print_range("drop(3)", skip3);   // 4 5 6 7 8 9 10

        // 与 filter 组合:取前3个偶数
        auto first3even = data
            | v::filter([](int x) { return x % 2 == 0; })
            | v::take(3);
        print_range("前3个偶数", first3even);  // 2 4 6
    }

    // ================================================================
    // 5. take_while ------ 取满足条件的连续前缀
    //    drop_while ------ 丢弃满足条件的连续前缀
    // ================================================================
    {
        auto prefix = data | v::take_while([](int x) { return x < 5; });
        print_range("take_while(x < 5)", prefix);  // 1 2 3 4

        auto suffix = data | v::drop_while([](int x) { return x < 5; });
        print_range("drop_while(x < 5)", suffix);  // 5 6 7 8 9 10
    }

    // ================================================================
    // 6. reverse ------ 反转 range(要求 bidirectional_range)
    // ================================================================
    {
        auto reversed = data | v::reverse;
        print_range("reverse", reversed);  // 10 9 8 7 6 5 4 3 2 1
    }

    // ================================================================
    // 7. split ------ 按分隔符切割 range
    //    返回的是一个 range of ranges,每个子 range 是一段
    // ================================================================
    {
        std::string text = "hello world cpp ranges";
        auto words = text | v::split(' ');
        std::cout << "split(' '): ";
        for (auto word : words) {
            std::cout << '\'';
            for (char ch : word) std::cout << ch;
            std::cout << "' ";
        }
        std::cout << '\n';  // 'hello' 'world' 'cpp' 'ranges'
    }

    // ================================================================
    // 8. join ------ 展平嵌套的 range(range of ranges → flat range)
    // ================================================================
    {
        std::vector<std::vector<int>> nested = {{1, 2}, {3, 4, 5}, {6}};
        auto flat = nested | v::join;
        print_range("join(展平嵌套)", flat);  // 1 2 3 4 5 6

        // join 与 split 配合:先切割再展平(类似于去除分隔符)
        std::string text = "a-b-c";
        auto parts = text | v::split('-');
        // 把 '-' 替换为 ' '
        // 注意:这里 split 产生的 subrange 再 join 需要 C++23 的 join_with
        // 此处演示 join 的基本用法
    }

    // ================================================================
    // 9. keys / values ------ 提取关联容器的键/值
    // ================================================================
    {
        std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};

        std::cout << "keys: ";
        for (auto k : m | v::keys) std::cout << k << ' ';     // 1 2 3
        std::cout << '\n';

        std::cout << "values: ";
        for (auto v : m | v::values) std::cout << v << ' ';   // one two three
        std::cout << '\n';
    }

    // ================================================================
    // 10. all ------ 将任意 range 包装为 view
    //     当你想使用管道语法但手上的容器不是 view 时,all 是桥梁
    // ================================================================
    {
        auto v = data | v::all;          // vector → view
        auto r = v | v::reverse;         // 管道串联
        print_range("all | reverse", r); // 10 9 8 7 6 5 4 3 2 1
        // 实际上 | 运算符已经隐式做了这个转换,显式使用 all 的场景较少
    }

    // ================================================================
    // 11. common ------ 将 sentinel 类型与 iterator 类型统一
    //     某些遗留代码要求 begin() 和 end() 返回相同类型
    // ================================================================
    {
        auto filtered = data | v::filter([](int x) { return x > 5; });
        // filtered 的 begin() 和 end() 类型不同(sentinel != iterator)
        // static_assert(!r::common_range<decltype(filtered)>);

        auto common = filtered | v::common;
        // common 的 begin() 和 end() 返回相同类型
        static_assert(r::common_range<decltype(common)>);

        print_range("common", common);  // 6 7 8 9 10
    }

    // ================================================================
    // 12. 投射(Projection) ------ 不是适配器,但常与 Range 算法配合
    //     所有 std::ranges 算法都支持 projection 参数
    // ================================================================
    {
        struct Person { std::string name; int age; };
        std::vector<Person> people = {
            {"Charlie", 35}, {"Alice", 30}, {"Bob", 25}
        };

        // 按年龄排序,使用 projection 而非手写 lambda
        r::sort(people, std::less{}, &Person::age);

        std::cout << "projection(按age排序): ";
        for (auto& p : people) std::cout << p.name << '(' << p.age << ") ";
        std::cout << '\n';
    }

    return 0;
}

工厂视图

自身生成 range

工厂 功能 典型场景
iota(start) 无限递增序列 [start, ∞) 配合 take 生成索引
iota(start, end) 有限递增序列 [start, end) 生成数值区间
empty<T> 空 range 占位/空结果
single(x) 只包含单一元素的 range 插入单元素到管道
istream<T>(stream) 从输入流惰性读取 处理大数据文件
counted(it, n) 迭代器 + 计数构造 range 对接指针+长度 API

代码示例:

cpp 复制代码
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <sstream>
#include <iterator>
#include <cassert>

namespace r = std::ranges;
namespace v = std::views;

template<typename R>
void print_range(std::string_view name, R&& r) {
    std::cout << name << ": ";
    for (auto&& e : r) std::cout << e << ' ';
    std::cout << '\n';
}

// ----------------------------------------------------------------------------
int main() {

    // ================================================================
    // 1. iota ------ 生成递增序列,类似 Python 的 range()
    //    语法: v::iota(start, end)  生成 [start, end) 的整数
    //    也可: v::iota(start)       生成 [start, ∞) 的无界序列
    // ================================================================
    {
        // 有界 iota:指定上限
        auto seq = v::iota(1, 11);        // 1, 2, 3, ..., 10
        print_range("iota(1, 11)", seq);  // 1 2 3 4 5 6 7 8 9 10

        // 无界 iota:从某个值开始,无限延伸(必须配合 take 使用)
        // 直接迭代无界 iota 会导致无限循环!
        auto inf_seq = v::iota(100);               // 100, 101, 102, ...
        auto first5 = inf_seq | v::take(5);
        print_range("iota(100) | take(5)", first5); // 100 101 102 103 104
    }

    // ================================================================
    // 1b. iota 不只是整数 ------ 任何支持 ++ 和 < 的类型都可用
    // ================================================================
    {
        // 生成大写字母序列
        auto alpha = v::iota('A', 'H');     // A, B, C, D, E, F, G
        std::cout << "iota('A','H'): ";
        for (char c : alpha) std::cout << c << ' ';
        std::cout << '\n';
    }

    // ================================================================
    // 2. empty<T> ------ 生成空 range(不包含任何元素)
    //    常用于需要"空结果"占位的场景
    // ================================================================
    {
        auto e = v::empty<int>;
        std::cout << "empty<int> size: " << r::distance(e) << '\n'; // 0
        assert(r::empty(e));  // 确认是空 range
    }

    // ================================================================
    // 3. single(x) ------ 生成只包含单一元素的 range
    //    常与 join 配合使用,将单元素插入管道流
    // ================================================================
    {
        auto s = v::single(42);
        print_range("single(42)", s);  // 42

        static_assert(r::size(s) == 1);
        assert(*s.begin() == 42);
    }

    // ================================================================
    // 4. istream<T> ------ 从输入流读取,作为 range
    //    按需从流中惰性读取,不一次性加载所有数据
    // ================================================================
    {
        std::istringstream iss("10 20 30 40 50");
        auto numbers = v::istream<int>(iss);

        std::cout << "istream<int>: ";
        for (int x : numbers) std::cout << x << ' ';  // 10 20 30 40 50
        std::cout << '\n';
    }

    // ================================================================
    // 4b. istream 管道组合 ------ 从流读取后立即过滤/变换
    //     无需先存到 vector,直接惰性处理
    // ================================================================
    {
        std::istringstream iss("1 2 3 4 5 6 7 8 9 10");
        auto result = v::istream<int>(iss)
            | v::filter([](int x) { return x % 2 == 0; })
            | v::transform([](int x) { return x * x; })
            | v::take(3);

        std::cout << "istream管道(偶数的平方前3个): ";
        for (int x : result) std::cout << x << ' ';  // 4 16 36
        std::cout << '\n';
    }

    // ================================================================
    // 5. counted ------ 从迭代器 + 计数构造 range
    //    接收 (iterator, count) 对,生成大小固定的 range
    //    常用于处理遗留代码中"指针+长度"的 API
    // ================================================================
    {
        std::vector<int> v = {10, 20, 30, 40, 50, 60, 70, 80};
        // 从第3个元素(30)开始,取4个
        auto sub = v::counted(v.begin() + 2, 4);
        print_range("counted(begin+2, 4)", sub);  // 30 40 50 60

        // 实质等价于 r::subrange(begin, begin + count)
    }

    // ================================================================
    // 6. 综合示例:工厂 + 适配器 构建完整管道
    //    生成前20个自然数,保留3的倍数,平方后取前5个
    // ================================================================
    {
        auto pipeline = v::iota(1)                     // 工厂: 1,2,3,...
            | v::filter([](int x) { return x % 3 == 0; })  // 保留3的倍数: 3,6,9,...
            | v::transform([](int x) { return x * x; })     // 平方: 9,36,81,...
            | v::take(5);                                   // 取前5个

        print_range("综合管道", pipeline);  // 9 36 81 144 225
    }

    // ================================================================
    // 6b. 综合示例:斐波那契数列生成
    //     利用 iota 和 transform 可以生成任意序列
    // ================================================================
    {
        // 技巧: iota(0) 提供索引 n=0,1,2,...,transform 将 n 映射为 Fib(n)
        // 但这里需要状态,用单行管道做不直观,展示另一种思路:
        // 用 iota 产生索引,通过迭代计算 Fib

        // 更直接的 Fibonacci:使用结构化绑定 + iota
        auto fib = v::iota(0)
            | v::transform([a = 0ull, b = 1ull](int) mutable {
                  auto result = a;
                  a = b;
                  b = result + b;
                  return result;
              })
            | v::take(10);

        std::cout << "Fibonacci(前10项): ";
        for (auto f : fib) std::cout << f << ' ';  // 0 1 1 2 3 5 8 13 21 34
        std::cout << '\n';
    }
    return 0;
}

综合实践

按部分筛选员工然后加薪资

cpp 复制代码
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <map>
#include <ranges>
#include <string>
#include <vector>

namespace r = std::ranges;
namespace v = std::views;

// ============================================================================
// 员工薪资分析系统 --- C++20 Ranges 综合应用
// ============================================================================

struct Employee {
    std::string name;
    std::string department;
    double salary;
};

// ============================================================================
// 功能1: 按部门分组统计平均薪资
// ============================================================================
// C++20 没有 std::views::group_by(那是 C++23 的特性),因此采用 map 聚合,
// 配合 range-based for 遍历。每个部门的 salary 被累加后求均值。

void avgSalaryByDept(const std::vector<Employee>& employees) {
    // <部门, {薪资总和, 人数}>
    std::map<std::string, std::pair<double, int>> dept_stats;

    for (const auto& emp : employees) {
        auto& [total, count] = dept_stats[emp.department];
        total += emp.salary;
        ++count;
    }

    std::cout << "\n===== 各部门平均薪资 =====\n";
    std::cout << std::left;
    for (const auto& [dept, stats] : dept_stats) {
        const auto& [total, count] = stats;
        std::cout << "  " << std::setw(12) << dept
                  << " : " << std::fixed << std::setprecision(2)
                  << total / count << '\n';
    }
}

// ============================================================================
// 功能2: 找出薪资最高的员工
// ============================================================================
// 使用 std::ranges::max_element,通过 projection 指定比较对象为 salary 成员。
// 比传统 lambda 版本更简洁: r::max_element(v, less{}, &Employee::salary)

void findHighestPaid(const std::vector<Employee>& employees) {
    auto it = r::max_element(employees, std::less{}, &Employee::salary);

    std::cout << "\n===== 薪资最高员工 =====\n";
    if (it == employees.end()) {
        std::cout << "  (无数据)\n";
        return;
    }

    std::cout << "  " << it->name
              << " (" << it->department << ")"
              << " : " << std::fixed << std::setprecision(2)
              << it->salary << '\n';
}

// ============================================================================
// 功能3: 筛选技术部员工并加薪10%,然后打印
// ============================================================================
// filter 视图筛选技术部,展开 for 循环中修改 salary 后输出新旧对比。
// 注意: filter 返回的是 view,对 view 中元素的修改会反映回原容器。

void raiseITDept(std::vector<Employee>& employees) {
    std::cout << "\n===== 技术部加薪10% =====\n";
    std::cout << std::left;

    // 管道: 保留技术部 → 遍历修改
    auto it_emps = employees
        | v::filter([](const Employee& e) { return e.department == "技术部"; });

    for (auto& emp : it_emps) {
        double old_salary = emp.salary;
        emp.salary *= 1.10;
        std::cout << "  " << std::setw(10) << emp.name
                  << " : " << std::setw(8) << old_salary
                  << " -> " << std::fixed << std::setprecision(2)
                  << emp.salary << '\n';
    }
}

// ============================================================================
// 统一入口: 依次执行上述三项分析
// ============================================================================

void analyzeEmployees(std::vector<Employee>& employees) {
    avgSalaryByDept(employees);
    findHighestPaid(employees);
    raiseITDept(employees);
}

// ============================================================================
// main
// ============================================================================

int main() {
    std::vector<Employee> staff = {
        {"张三",   "技术部", 25000.0},
        {"李四",   "技术部", 28000.0},
        {"王五",   "技术部", 22000.0},
        {"赵六",   "市场部", 18000.0},
        {"孙七",   "市场部", 20000.0},
        {"周八",   "人事部", 15000.0},
        {"吴九",   "技术部", 35000.0},
        {"郑十",   "人事部", 16000.0},
        {"冯十一", "市场部", 19000.0},
    };

    std::cout << std::fixed << std::setprecision(2);
    std::cout << "===== 员工薪资分析系统 =====\n";
    std::cout << "共 " << staff.size() << " 名员工\n";

    analyzeEmployees(staff);
}

本期内容到这里就结束了,喜欢请点个赞谢谢

封面图自取:

相关推荐
Chenyiax13 分钟前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH14 分钟前
Koa和Express的区别
后端
MariaH20 分钟前
Koa框架的使用
后端
luckdewei1 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某3 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy3 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom3 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户1474853079747 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody1237 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端
onething3658 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈