26.高级特性(上)

目录

  • 一、不安全的Rust
  • 二、不安全的超能力
    • 2.1 概念
    • 2.2 解引用裸指针
    • 2.3 调用不安全的函数或方法
    • 2.3 创建不安全代码的安全抽象
    • 2.4 使用extern函数调用外部代码
    • 2.5 访问或修改可变静态变量
    • 2.6 实现不安全trait
    • 2.7 访问联合体中的字段
  • 三、高级trait
    • 3.1 关联类型在trait定义中指定占位符类型
    • 3.2 关联类型与泛型的区别
    • 3.3 默认泛型类型参数和运算符重载
    • 3.4 完全限定语言与消歧义:如何调用相同名称的方法
    • 3.5 使用supertrait来要求trait附带其它trait的功能
    • 3.6 使用newtype模式用于在外部类型上实现外部trait

一、不安全的Rust

  • Rust隐藏有第二种语言,它没有强制内存安全保证,这被称为不安全Rust(unsafe Rust);
  • 不安全Rust存在的原因:
    • 静态分析是保守的,使用unsafe Rust则是告诉编译器自己知道在做啥;
    • 计算机硬件本身是不安全的, Rust需要能够进行底层系统编程;

二、不安全的超能力

2.1 概念

  • 可以通过unsafe关键字将Rust切换到不安全Rust,开启一个块,存放不安全代码;
  • Unsafe Rust里执行的五种操作
    1. 解引用裸指针;
    2. 调用不安全的函数或方法;
    3. 访问或修改可变静态变量;
    4. 实现不安全trait;
    5. 访问union的字段;
  • unsafe 并不会关闭借用检查器或禁用任何其他安全检查;
  • unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能;
  • 隔离unsafe代码,最好将它们封装进一个安全的抽象并提供安全API;

2.2 解引用裸指针

  • 可变的原始指针: *mut T;
  • 不可变的原始指针: *const T;
  • 不可变意味着指针解引用之后不能直接赋值;
  • *不是解引用运算符,它是类型名称的一部分;
  • 裸指针与引用和智能指针的区别:
    • 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针;
    • 不保证指向有效的内存;
    • 允许为空;
    • 不能实现任何自动清理功能;
fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
    let address = 0x012345usize;
    let r = address as *const i32;
//    println!("r1 is: {}", *r1);
//    println!("r2 is: {}", *r2);
}
  • 代码同时创建不可变和可变裸指针;
  • 可以在安全代码中创建裸指针,但是解引用必须在unsafe代码块里;
  • 使用as将不可变和可变引用强制转换为对应的裸指针类型;
  • address是一个不能确定其有效性的裸指针;
  • 放开最后两行注释而直接进行解引用会产生错误;

在这里插入图片描述

  • 使用unsafe {}把他们包裹起来就能正确运行了;
  • 为什么要用原始指针?
    • 与C语言进行交互;
    • 构建借用检查器无法理解的安全抽象;

2.3 调用不安全的函数或方法

  • unsafe函数或方法:在定义前加上unsafe关键字;
  • 调用unsafe函数要先满足条件(看文档),Rust无法对这些条件进行验证;
  • 需要在unsafe块里调用;
unsafe fn dangerous() {}

fn main() {
   dangerous();
}
  • 编译错误
  • 在main函数里,用unsafe{} 将dangerous函数调用包裹起来就能通过了;

在这里插入图片描述

2.3 创建不安全代码的安全抽象

  • 函数包含不安全代码并不意味着整个函数都需要标记为不安全;
  • 将不安全代码封装进安全函数是一个常见的抽象;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    
    let len = slice.len();
    
    assert!(mid <= len);

    (&mut slice[..mid],
     &mut slice[mid..])
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    let (a, b) = split_at_mut(&mut v[..], 3);
    
    println!("a = {:?}", a);
    println!("b = {:?}", b);
}
  • 上述代码中split_at_mut函数将传入的可变切片进行分割,返回两个可变切片;
  • 编译报错

在这里插入图片描述

  • Rust的借用检查器任何代码将同一个变量借用了两次;
  • 代码里明确了两个切片不会重叠,这就要触及不安全代码了;
  • 这就需要用unsafe块,裸指针和一些不安全函数调用来修改split_at_mut
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (slice::from_raw_parts_mut(ptr, mid),
         slice::from_raw_parts_mut(ptr.add(mid), len - mid))
    }
}
  • 使用len方法获取切片的长度,使用as_mut_ptr获取访问切片的裸指针;
    • slice变量是一个i32类型的可变切片,因此as_mut_ptr返回一个*mut i32类型的裸指针,储存在 ptr 变量中;
  • slice::from_raw_parts_mut函数获取一个裸指针和一个长度来创建一个切片;
  • 无需将split_at_mut函数标记为unsafe,且该函数可以在安全的Rust中被调用;

下面这段代码使用slice时会崩溃

use std::slice;

fn main() {
    let address = 0x01234usize;
    let r = address as *mut i32;
    
    let slice: &[i32]  = unsafe {
        slice::from_raw_parts_mut(r, 10000)
    };

    println!("slice = {:#?}", slice);
}

编译正确,运行崩溃
在这里插入图片描述

2.4 使用extern函数调用外部代码

  • extern函数可以创建和使用外部函数接口;
  • extern块中声明的函数在 Rust 代码中总是不安全的;
  • 外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
  • 上述代码展示了如何集成C标准库中的abs函数;
  • extern "C"块中,列出了要调用的C语言中的外部函数;

从其它语言调用Rust函数

  • 使用extern创建一个允许其他语言调用 Rust 函数的接口;
  • 还需增加#[no_mangle]标注;
    • Mangling是指当编译器将代码中指定的函数名进改时会增加一些额外的信息;
    • 每一个编程语言的编译器都会以稍微不同的方式mangle函数名;
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
  • 将上述代码编译为动态库并从C语言中链接,call_from_c函数就能够在 C 代码中访问;
  • extern 的使用无需使用unsafe标注;

2.5 访问或修改可变静态变量

  • 如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争;
  • 全局变量在 Rust 中被称为 静态(static)变量
  • 通常静态变量的名称采用SCREAMING_SNAKE_CASE写法;
  • 静态变量只能储存拥有static生命周期的引用,因此静态变量的生命周期可以被Rust自动计算;
  • 访问不可变静态变量是安全的,访问和修改可变静态变量都是不安全的;
static mut COUNTER: u32 = 0;
static HELLO_WORLD: &str = "Hello, world!";

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }

    println!("name is: {}", HELLO_WORLD);
}
  • 代码展示了不可变静态变量HELLO_WORLD的声明和使用;
  • 任何访问或修改可变静态变量COUNTER的代码都必须位于unsafe中;

2.6 实现不安全trait

  • 当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的;
  • 在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也应该标记为unsafe;

2.7 访问联合体中的字段

参考文档:https://rustwiki.org/zh-CN/reference/items/unions.html

三、高级trait

3.1 关联类型在trait定义中指定占位符类型

  • 关联类型(associated types) 是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法参数中就可以使用这些占位符类型;
  • trait的实现者会针对特定的实现在这个类型的位置指定相应的具体类型;
  • 标准库提供的Iterator trait就是一个带有关联类型的trait,内部的关联类型Item替代遍历的值的类型;
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • Item是占位符,next的返回值说明它返回Option<Self::Item>类型的值;
  • trait的实现者指定Item的具体类型;
  • 用起来像泛型;

3.2 关联类型与泛型的区别

泛型关联类型
每次实现Trait时标注类型无需标注类型
可以为一个类型多次实现某个Trait(不同的泛型参数)无法为单个类型多次实现某个 Trait
pub trait Iterator2<T>{
    fn next(&mut self) -> Option<T>;
}

struct Counter{}

impl Iterator2<String> for Counter{
    fn next(&mut self) -> Option<String> {
        None
    }
}

impl Iterator2<u32> for Counter{
    fn next(&mut self) -> Option<u32> {
        None
    }
}
  • 上述代码为 Counter实现了Iterator2的trait,返回值为String和u32;
pub trait Iterator{
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter{}

impl Iterator for Counter{
    
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        None    
    }
}
  • 上述代码实现了关联类型,只能写一个,如果再写一遍impl Iterator for Counter{,里面的Item写成String,就会报错;

3.3 默认泛型类型参数和运算符重载

  • 可以在使用泛型参数的时候为泛型指定一个默认的具体类型;
  • 语法为:<PlaceholderType=ConcreteType>
  • 这种技术通常用于运算符重载
  • Rust不允许创建自己的运算符及重载任意的运算符;
  • 但可以通过std::ops中所列出的运算符和相应的 trait重载一部分相应的运算符;
use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
               Point { x: 3, y: 3 });
}
  • 上述代码在Point结构体上实现Add trait来重载+运算符;
  • Add trait定义如下
#[doc(alias = "+")]
#[const_trait]
pub trait Add<Rhs = Self> {
    /// The resulting type after applying the `+` operator.
    #[stable(feature = "rust1", since = "1.0.0")]
    type Output;

    #[must_use = "this returns the result of the operation, without modifying the original"]
    #[rustc_diagnostic_item = "add"]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn add(self, rhs: Rhs) -> Self::Output;
}
  • Rhs = Self默认类型参数
  • RHS是一个泛型类型参数,它用于定义 add 方法中的 rhs 参数;
  • 如果实现 Add trait 时不指定 RHS 的具体类型,RHS 的类型将是默认的 Self 类型,也就是Point;
  • 下面是一个实现Add trait时使用自定义类型而不是默认类型的例子;
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
  • 这是将毫米和米相加的例子;
  • 所以用Add<Meters>指明是米,相加的时候传进来的是米,所以乘以1000再相加;
  • 返回值是毫米;

默认泛型参数的主要应用场景

  • 扩展一个类型而不破坏现有代码;
  • 允许在大部分用户都不需要的特定场景下进行自定义;

3.4 完全限定语言与消歧义:如何调用相同名称的方法

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}
  • PilotWizard都有fly方法;
  • Human又定义了一个fly
  • 几个方法的参数是完全相同的,那么调用哪个?
    在这里插入图片描述
  • 所以调用的是Human本身的fly方法;
  • 如下代码演示了调用Pilot和Wizard的fly方法;
fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
  • 当同一作用域的两个类型都实现了同一trait,Rust就不能明确的知道调用哪个函数;
  • 使用完全限定语法(fully qualified syntax) 可以解决这个问题;
  • 语法为:<Type as Trait>::function(receiver_if_method, next_arg, ...);
    • 可以在任何调用函数或方法的地方使用;
    • 允许忽略那些从其它上下文能推导出来的部分;
    • 当Rust无法区分代码编写人员期望调用哪个具体实现时,才需要使用这种语法;
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name()); //A baby dog is called a Spot
    //println!("A baby dog is called a {}", Animal::baby_name());
}
  • Animal trait有关联函数baby_name,结构体 Dog 实现了 Animal,都是一些关联方法(没有self);
  • baby_name直接在Dog之上,因此使用Dog::baby_name直接调用就可以;
  • 放开最后一个println!,则无法编译通过;
    在这里插入图片描述
  • 使用完全限制语法解决它:println!("A baby dog is called a {}", <Dog as Animal>::baby_name());

3.5 使用supertrait来要求trait附带其它trait的功能

  • 需要在一个trait中使用其它trait的功能;
    • 需要被依赖的trait也被实现
    • 那个被间接依赖的trait就是当前trait的supertrait;
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}
  • OutlinePrint trait里的outline_print函数要被使用,则必须实现fmt::Displaytrait;
  • 结构体Point实现了OutlinePrint,但它没有实现Displaytrait,所以会报错;
    在这里插入图片描述
  • 在Point上实现了Display后就会通过编译;
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

3.6 使用newtype模式用于在外部类型上实现外部trait

  • 孤独规则: 只有当trait或类型定义在本地包时,才能为该类型实现这个trait;
  • 可以通过newtype模式绕过这个规则;
    • 利用tuple struct元组结构体创建一个新的类型;
use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}
  • 想在Vec<T>上实现Display确被孤独规则阻止(Display trait和Vec<T>都定义于外面的包中);
  • 可以创建一个包含Vec<T>实例的 Wrapper 结构体, 在它之上实现 Display 并使用Vec<T>的值;

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Cesium--旋转3dtiles

以下代码来自Cesium 论坛&#xff1a;3DTileset rotation - CesiumJS - Cesium Community 在1.118中测试可行&#xff0c;可直接在Sandcastle中运行&#xff1a; const viewer new Cesium.Viewer("cesiumContainer", {terrain: Cesium.Terrain.fromWorldTerrain()…

模拟面试之外卖点单系统(高频面试题目mark带答案)

昨天跟大家分享一个大家简历中常见的项目-《外卖点单系统》&#xff0c;这是一个很经典的项目&#xff0c;有很多可以考察的知识点和技能点&#xff0c;但大多数同学都是学期项目&#xff0c;没有实际落地&#xff0c;对面试问题准备不充分&#xff0c;回答时抓不到重点&#x…

集群分布式储存

硬件&#xff1a; 存储柜 软件 &#xff1a; software define storage 分布式存储 是一种独特的系统架构由一组能够通过网络连通&#xff0c;为了完成共同任务而协调任务的计算机节点组成分布式是为了使用廉价的普通的计算机完成复杂的计算和存储任务目的就是利用更多的机…

Java-异常:不恰当的异常转换、不充分的日志记录、过度或不当的异常捕获

Java-异常&#xff1a;不恰当的异常转换、不充分的日志记录、过度或不当的异常捕获 Java-异常&#xff1a;不恰当的异常转换、不充分的日志记录、过度或不当的异常捕获一、前期准备二、案例分析1、不恰当的异常转换2、不充分日志记录3、过度或不当的异常捕获 三、正确处理方式1…

常见图像分割模型介绍:FCN、U-Net、SegNet、Mask R-CNN

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

《计算机英语》 Unit 5 Networking 网络

Section A Networking 网络 The need to share information and resources among different computers has led to linked computer systems, called networks, in which computers are connected so that data can be transferred from machine to machine. 不同计算机之间共享…

基于SpringBoot+Vue的美容美发在线预约系统的设计与实现【附源码】

毕业设计(论文) 题目&#xff1a;基于SpringBootVue的美容美发在线预约系统的设计与实现 二级学院&#xff1a; 专业(方向)&#xff1a; 班 级&#xff1a; 学 生&#xff1a; 指导教师&#xff…

【ARMv8/v9 GIC 系列 2.3 -- GIC SPI 中断的 GICD_CLRSPI_NSR寄存器】

文章目录 GICD_CLRSPIN_NSR寄存器功能INTID 位 [12:0]中断触发类型的影响小结 GICD_CLRSPIN_NSR 在 ARMv9 架构下&#xff0c;GIC&#xff08;Generic Interrupt Controller&#xff09;是负责中断管理的关键组件&#xff0c;它支持复杂的中断处理需求&#xff0c;包括多处理器…

Vue 鼠标滑入元素改变其背景颜色,且鼠标划入另一块区域,背景颜色保持不变

如上图所示&#xff1a;鼠标划入"条件区域",对应ul元素改变背景颜色&#xff0c;且划入内容区域时&#xff0c;ul元素的背景颜色保持不变。只有当鼠标划出"内容区域"&#xff0c;或者切换到"条件区域"的其他ul元素上时&#xff0c;背景颜色才恢复…

Android开发系列(九)Jetpack Compose之ConstraintLayout

ConstraintLayout是一个用于构建复杂布局的组件。它通过将子视图限制在给定的约束条件下来定位和排列视图。 使用ConstraintLayout&#xff0c;您可以通过定义视图之间的约束关系来指定它们的位置。这些约束可以是水平和垂直的对齐、边距、宽度和高度等。这允许您创建灵活而响…

小阿轩yx-用户管理与高级SQL语句

小阿轩yx-用户管理与高级SQL语句 MySQL 进阶查询 运维工作中可以提供不小的帮助&#xff0c;运维身兼数职&#xff0c;可能会有不少数据库的相关工作 常用查询介绍 对查询的结果集进行处理 按关键字排序 使用 SELECT 语句可以将需要的数据从 MySQL 数据库中查询出来 对结…

嘀嗒出行项目管理专家和项目管理负责人王禹华受邀为第十三届中国PMO大会演讲嘉宾

全国PMO专业人士年度盛会 嘀嗒出行项目管理专家和项目管理负责人王禹华女士受邀为第十三届中国PMO大会演讲嘉宾&#xff0c;演讲议题为“AI时代项目经理挑战机会和个人成长”。大会将于6月29-30日在北京举办&#xff0c;敬请关注&#xff01; 议题简要&#xff1a; AI时代对互…

vue3+ts:监听dom宽高变化函数

一、效果展示 二、代码 getSize.ts import { ref, Ref, watchEffect } from "vue";export const getWidth (domRef: Ref<HTMLElement | null>) > {const width ref<number>(0);const height ref<number>(0);const observer new ResizeObs…

【代码随想录】【算法训练营】【第50天】 [1143]最长公共子序列 [1035]不相交的线 [53]买卖股票的最佳时机III [392]判断子序列

前言 思路及算法思维&#xff0c;指路 代码随想录。 题目来自 LeetCode。 day 50&#xff0c;周三&#xff0c;无法坚持~ 题目详情 [1143] 最长公共子序列 题目描述 1143 最长公共子序列 解题思路 前提&#xff1a; 思路&#xff1a; 重点&#xff1a; 代码实现 C语…

浦语·灵笔2 模型部署图片理解实战

效果图镇楼 1、使用 huggingface_hub 下载模型中的部分文件&#xff08;演示练习与模型实战无关&#xff09; 使用 Hugging Face 官方提供的 huggingface-cli 命令行工具。安装依赖: pip install -U huggingface_hub 然后新建 python 文件&#xff0c;填入以下代码&#xf…

dwg文件转换的软件,分享4款软件!

在数字化设计领域&#xff0c;DWG文件作为CAD&#xff08;计算机辅助设计&#xff09;的核心文件格式&#xff0c;其重要性不言而喻。然而&#xff0c;在实际应用中&#xff0c;我们有时需要将DWG文件转换为其他格式以便于分享、展示或进行其他操作。那么&#xff0c;DWG文件转…

【自然语言处理系列】探索NLP:使用Spacy进行分词、分句、词性标注和命名实体识别,并以《傲慢与偏见》与全球恐怖活动两个实例文本进行分析

本文深入探讨了scaPy库在文本分析和数据可视化方面的应用。首先&#xff0c;我们通过简单的文本处理任务&#xff0c;如分词和分句&#xff0c;来展示scaPy的基本功能。接着&#xff0c;我们利用scaPy的命名实体识别和词性标注功能&#xff0c;分析了Jane Austen的经典小说《傲…

(七)React:useEffect的理解和使用

1. useEffect的概念理解 useEffect是一个React Hook函数&#xff0c;用于React组件中创建不是由事件引起而是由渲染本身引起的操作&#xff0c;比如发送AJAX请求&#xff0c;更改DOM等等 说明&#xff1a;上面的组件中没有发生任何的用户事件&#xff0c;组件渲染完毕之后就需…

Python学习笔记20:进阶篇(九)常见标准库使用之sys模块和re模块

前言 本文是根据python官方教程中标准库模块的介绍&#xff0c;自己查询资料并整理&#xff0c;编写代码示例做出的学习笔记。 根据模块知识&#xff0c;一次讲解单个或者多个模块的内容。 教程链接&#xff1a;https://docs.python.org/zh-cn/3/tutorial/index.html 错误输出…

【已解决】Python报错:AttributeError: module ‘json‘ has no attribute ‘loads‘

&#x1f60e; 作者介绍&#xff1a;我是程序员行者孙&#xff0c;一个热爱分享技术的制能工人。计算机本硕&#xff0c;人工制能研究生。公众号&#xff1a;AI Sun&#xff0c;视频号&#xff1a;AI-行者Sun &#x1f388; 本文专栏&#xff1a;本文收录于《AI实战中的各种bug…