zoukankan      html  css  js  c++  java
  • rust语法

    rust语法

    前言

    rust是什么?rust是一门新型的编程语言,目的是取代c/c++做为一个系统开发级别的编程语言。它有着c的底层高效,有着c++的现代描述能力,同时保持足够的洁简。作为一个新语言,他在吸取现代语言精华的同时,也避免其中设计中的一些繁琐之处,同时它已经被许多大型的开发项目所采用,有着良好的商业化前景。

    一、数据类型

    rust属于静态类型语言。即编译时必须知道所有变量的类型。

    let 模式:数据类型 = 表达式; //定义不变量
    let mut 模式:数据类型 = 表达式; //定义变量
    

    1.1 标量scalar

    1.1.1 整型

    长度 有符号 无符号
    8-bit i8 u8
    16-bit i16 u16
    32-bit i32 u32
    64-bit i64 u64
    架构 isize usize

    1.1.2 浮点数

    长度 名称
    32-bit f32
    64-bit f64

    采用IEEE-754标准表示。

    1.1.3 布尔

    类型 真值 假值
    bool true false

    1.1.4 字符类型

    类型 值样例
    char 'A'

    字符是unicode标准编码的标量值。

    1.2 复合compound

    将多个数据类型组合成一个新的类型。

    1.2.1 元组tuple

    类型样例 值样例
    (i32,char,u8) (500,'B',1)

    模式解构元组:

    let (x,y,z)=(1,'A',3);

    1.2.2 数组array

    类型样例 值样例
    [i64;5] [1,2,3,4,5]

    数组是固定长度,且元素类型相同。

    1.2.3 结构struct

    类型样例 值样例
    struct User{字段序列} User{键值对序列}
    struct User{age:u8,active:bool} User{age:18,active:true}
    struct User(u8,bool); User(18,true)
    struct User; User

    1.2.4 枚举enum

    类型样例 值样例
    enum 名称{类结构序列} enum Message{Quit,Move{x:u8,y:u8},Write(u8,u8)}

    1.3 切片slice

    对数组的某个片段建立投影视图的类型。切片是动态大小类型,因此只能使用切片引用。

    类型样例 值样例
    str let a:&str = &"hello"[2..4];
    [T] let a:&[i8] = &[1,2,3][0..3];

    1.4 引用(借用)reference

    引用是指针(保存地址),指向具体数据。在rust中,引用是没有数据所有权,而只是”借用“使用权的特殊类型。

    类型样例 值样例
    &类型 let i :&int= &3; let j :int=*i;

    1.5 智能指针smart pointers

    智能指针指的是能自动释放相关资源的一系列数据结构。它表现形式类似指针,但大多数情况都拥有数据所有权,因此概念上和rust引用不相干。

    常见智能指针:

    • Box : 用于在堆上分配值
    • Rc :引用计数,可实现多个拥有者
    • Ref、RfeMut、RefCell : 运行时借用规则
    • String :字符串
    • Vec :向量集合

    用途:嵌套类型、大数据转移所有权、特征接口。

    智能指针一般需要实现:解引用特征和析构特征。

    解引用特征:

    use std::ops::Deref;//解不可变引用
    // DerefMut //解可变引用
    struct MyBox<T>(T);
    impl<T> Deref for MyBox<T>{
        type Target = T;
    
        fn deref(&self)->&T{
            &self.0
        }
    }
    let a = Mybox(3);
    let b = *a;// *a = *(a.deref()) = *(&T) = 3 
    
    //传参时会自动解引用强制多态deref coercions。
    fn f(x:&i32){println!("x={}",*x);}
    f(&a); // &a = &(*a) = &(3)
    

    析构特征:

    impl<T> Drop for MyBox<T>{
        fn drop(&mut self){
            println!("自动释放资源。");
        }
    }
    
    fn main(){
        let c = MyBox(3);
        drop(c);//手动释放
    }
    

    1.6 原生指针raw pointers

    rust通过引用来实现move的补充“借用”,同时用规则和编译检查来保证引用的安全性。但是某些时候还是需要类似c语言的原生指针。

    let mut a=3;
    let b = &a as *const i32; //指向不可变数据
    let c = &mut a as *mut i32;//指向可变数据
    let d = 0x234usize;
    let e = d as *const i32;//指向指定地址。
    unsafe{//只能在不安全块中解引用,得到指向的数据
        println!("*b={},*e={}", *b, *e);
    }
    

    1.7 函数指针

    fn fa(f:fn(i32)->i32, a:i32)->i32{
        f(a) + f(a) //通过函数指针f调用函数
    }
    fn fb(x:i32)->i32{
        x + x
    }
    let a = fa(fb, 5);//a=20;
    

    1.8 高级类型

    rust 特殊用途的类型。

    //别名类型。可命名的等价类型
    type Myint = i32;
    
    //从不返回类型never type:"!"
    fn bar() ->!{
        loop{}
    }
    
    //动态大小类型:str、特征
    let s:str = "hi"; //error.无法确定str类型静态大小
    let s:&str = "hi"; //&str 由指针和长度组成,大小等于 usize*2
    let s:&std::fmt::Display = &"hi"; //str实现了Display特征,而特征是动态大小,所以只能建立特征的引用
    
    //用于确定范型参数大小的特征:sized
    fn f<T:sized>(x:T){} //sized说明T是有静态大小的,sized可省略,自动添加
    fn f2<T:?sized>(x:&T){}//?sized表示sized可选,因而x只能是引用类型&T。
    

    二、语法结构

    2.1 模式匹配

    rust通过模式匹配来实现灵活的判断和定义变量。

    模式种类:

    1. 不可反驳irrefutable,即必然匹配,用于定义
    2. 可反驳refutable,可选匹配,用于判断
    let a=3; //模式a必然匹配
    if let Some(b)=None{}; //模式Some(b)可以不匹配
    match a{
        1 => ,//字面值模式
        1 | 2 => , //1 或 2
        4...9 => , //4 到 9
        3 if 3<2 =>, //匹配守卫,添加更多判断
        _ => ,//必然匹配占位符
    }
    
    //对复合结构的解构匹配
    struct Point{
        x:i32,
        y:i32,
    }
    let c = Point{x:1,y:2};
    let Point{x,y} = c; //解构出x=1, y=2两个变量
    
    let &Point{x:x2,y:y2}= &c;//对引用进行解构,得到x2,y2
    let (_,d,..)=(1,2,3,4,5); //解构元组,忽略第一个值,得d=2,忽略剩余值  
    
    if let e@1...5 = d {println!("e={}",e)};//@绑定匹配的值
    

    2.2 函数

    格式 范例
    fn 函数名(参数列表)->返回类型{函数体} fn main(){}

    函数头部由函数名、参数列表和返回类型组成,参数列表是由逗号分隔的一系列变量组成;函数体由一系列语句(statements)和一个可选的表达式(expressions)结尾组成。

    语句没有返回值,以分号结尾。表达式有返回值,没有分号结尾。

    2.3 分支

    2.3.1 if 分支

    格式 范例
    if 布尔表达式 {真值语句体} else {假值语句体} if 5>4 {} else {}

    if分支结构只会执行其中一条分支。if 分支结构是一个表达式。

    2.3.2 match 分支

    格式 范例
    match 变量{模式=>返回值,...} match m{Message::Quit=>1,Message::Write(x,y)=>x+y,Message::Move=>0}

    模式匹配必须穷尽所有情况。“_”下划线作为模式表示匹配所有。

    2.3.3 if let 分支

    格式 范例
    if let 模式=变量{...} if Message::Quit=m{1}

    if let匹配指定模式,忽略其他情况。

    2.4 循环

    格式 范例
    loop{循环语句体} loop{}
    while 布尔表达式{循环语句体} while 5>4 {}
    while let 模式{循环体} while let Some(t)= s.pop(){}
    for 元素 in 集合{循环语句体} for i in [1,2].iter(){}

    2.5 impl 块

    impl块可以将函数或方法关联到结构struct、枚举enum。

    格式 范例
    impl 结构名{函数列表} impl User{fn hello(&self){}}

    2.6 范型

    取代特定类型的占位符。通过特征trait描述占位符具备的通用能力。

    rust范型支持特例化技术。即对特定类型能定义不同的操作逻辑。

    范型最终会单态化monomorphization,即编译为特定类型的代码,因此不会有性能的开销,但会产生多份代码的数据占用。

    格式 范例
    基础结构 名称<占位符列表>... fn f<T,U>(x:T,y:U){}

    2.6.1 范型的默认类型

    trait Add<RHS=Self>{ //RHS默认类型是对象类型自身
        type Output;
        fn add(self,rhs:RHS)->Self::Output;
    }
    struct A(i32);
    impl Add for A{
        type Output = i32;
        fn add(self, r:A)->i32{self.0 + r.0}
    }
    let a = A(3);
    let b = A(4);
    println!("a+b={}", a.add(b));//a+b=7;
    

    2.7 特征trait

    trait 类似接口。

    1. 特征和实现他的类型之一必须位于本地项目crate。
    2. 特征可以有默认实现。
    3. 特征可以有关联类型,该类型类似范型占位符,但只有一个实现
    4. 特征可以有父特征

    特征定义:

    格式 范例
    trait 名称{声明序列} trait s{fn t();}

    实现特征:

    格式 范例
    impl 特征 for 结构{实现序列} impl s for m{fn t(){}}
    变量 :impl 特征 fn f(t :impl s){}
    范型占位符 : 特征 fn f<T:s>(t :T){}
    同上 fn f(t :T) where T:s{}

    2.7.1 特征的关联类型

    关联类型只允许一个实现,而范型可以有n个实现。

    trait IA{
        type Item;//关联类型
        fn print(&self)->Self::Item;
    }
    struct A;
    impl IA for A{
        type Item = i32;
        fn print(&self)->Self::Item{3}
    }
    let a = A;
    let b = a.print(); //b = 3i32
    

    2.8 特征对象trait object

    特征对象是rust面向对象技术的基础之一,实现同一接口多态调用。

    特征对象要求:

    1. 返回类型不为Self(即该特征对象的真实类型)
    2. 方法没有任何范型类型参数
    格式 范例
    &特征 let a:&Drop = &"3".to_string();

    2.9 闭包closures

    闭包是可以存储到变量,捕获调用者作用域的值的匿名函数特征。

    格式 范例
    |参数|{} let expr = |n|{n*2};
    fn fa()=>Box<Fn()->i32>{
        let a = 3;//局部变量
        Box::new(move||a) //闭包捕获a,但fa返回闭包,因此闭包生命周期比fa更长,默认捕获的a无效(通过引用),move关键字将a所有权转移到闭包。
        //Fn闭包是一种特征,因此无大小,需用智能指针Box包装
    }
    let a = fa()(); //a=3
    

    2.10 迭代器

    trait Iterator{
        type Item;
        fn next(&mut self) ->Option<Self::Item>;
    }
    
    迭代器 作用
    into_iter() 所有权迭代器
    iter() 可变引用迭代器,引用元素
    iter_mut() 可变引用迭代器,可变引用元素

    三、所有权ownership

    rust语言为了高效管理内存,使用所有权概念取代垃圾回收机制和手工管理内存。

    栈stack和堆heap是内存的两个区域。栈存放作用域上下文的变量,而堆存放自由分配的变量(通过指针使用)。

    rust的堆中变量赋值是移动move语义,而非浅拷贝shallow copy或深拷贝deep copy。

    因此所有权的意义是:值的所有者只有一个。该所有者离开作用域即丢弃值

    引用类型对move语义进行了补充,它不获得所有权,而只有使用权。为了保证数据有效性,编译器对引用对象的生命周期进行严格的检查和假定,默认函数无法通过引用返回对象。

    关于引用(&类型):

    1. 为了避免所有权的转移,使用引用类型来解决。
    2. 为了避免数据竞争data race,同一作用域变量如果有一个可变引用,即不可拥有其他引用。
    3. 编译器将会检查引用的有效性,避免悬垂引用。
    4. 为了增强编译器检查的能力(如函数返回引用),使用生命周期注解技术。

    获取值的三种方式:

    方式 所有权 生命周期 相关特征 关键字
    T 转移 自动 FnOnce move
    &T 编译检查 Fn
    &mut T 编译检查 FnMut

    3.1 生命周期lifetime

    变量都有生命周期,按定义的顺序在作用域末尾逆序终结。引用自身的生命周期必须短于所引用对象的生命周期,否则出现悬垂引用(指向无效对象的指针)。

    默认在同一个函数内可以通过借用检查器borrow checker自动判断,但是跨函数传递引用就无法自动处理。因为函数可被任意上下文调用,所以无法假定引用参数应该具备怎样的生命周期。

    生命周期注解是一种针对引用参数和拥有引用的复合结构与函数的范型占位符。它可以将几个变量的生命周期进行关联,声明具备“合理一致”的生命周期,从而让编译器取得检查引用有效性的必要信息。

    所谓“合理一致”指的编译器对关联的参数和返回值做出合理假设。如:

    1. 返回值是被注解的参数之一,因此“应该”拥有他们中生命周期最短的一个

    对于结构变量:

    1. 结构本身生命周期应该短于被注解的字段
    格式 范例
    &'注解 类型 fn f<'a>(x:&'a i32){}
    struct 结构<'占位符>{引用成员类型注解} struct s<'a>{p: &'a str,}
    fn 函数(注解参数列表)->注解返回类型{} fn f<'a>(x:&'a){}
    程序生命周期:&'static let s:&'static str="str";
    let a = 3;
    let mut p =&a;
    let mut p2;
    let b = 4;
    p = &b; //error,p比b长寿,因为b在p之后定义,即b先于p终结
    
    fn f<'a>(x:&'a isize,y:&'a isize)->&'a isize{
        x
    }
    p2 = f(&a,&b);//error, p2比f(a,b)长寿。因为虽然a比p2长寿,但b比p2短命,因此f(a,b)=b, b<p2,即使实际返回的是a
    
    struct C<'a>(mut &'a isize);
    let mut c = C(&b);
    let d = 5;
    c.0 = &d;//error, c.0 < c,被注解字段必须比结构长寿
    
    //生命周期子类型lifetime subtyping
    struct E<'e, 'c :'e>{c:&'e  C<'c>}
    struct E2<'e, T :'e>{c:&'e  T} //类似定义
    let e = E2{c:&c};
    fn fe(x:C)->&isize{
        E{c:&x}.c.0 //ok, 虽然E是临时变量,x是move进来的参数,但E的定义让其拥有两个格外生命周期:E::c 和C::0,其中E自身的生命周期对应临时变量,E::c对应x,而C::0只是被注解省略,对应->& isize
    }
    

    3.1.1 注解省略

    为了编码方便,以下情况可以省略注解:

    1. 单个待注解参数
    2. 其中一个参数是&self 或&mut self。

    四、包管理 cargo

    现代语言相对传统语言,个人认为最大的便利在于有一个设计良好的包管理系统。如何组织代码,是一门管理学问,将有助我们软件工程的建设。

    4.1 包 package

    ./Cargo.toml 工程描述文件,可以是一个项目,也可以是多个项目的工作区。

    [package]
    name="包名"
    version = "0.1.1"
    

    4.2 项目 crate

    项目一个到n个crate(箱)组成,一个crate对应一个.rs源文件。

    ./src/main.rs 项目根,0-n个
    ./src/lib.rs 项目根(库),0-1个
    ./src/bin/* 项目根(非默认项目)

    库项目可以被其他项目导入使用。

    一般同时需要修改工程描述文件,添加依赖:

    #Cargo.toml
    [dependencies]
    mylib = { path = "../mylib"}
    

    4.3 模块

    注意,当前rust语言有两个版本,最新版的模块系统已经进行了大改,请务必使用nightly每晚编译版本rust。

    crate可以划分模块。一个crate可以有1个到n个模块。默认模块名x对应x.rs,其中根为固定的”crate“名。

    例子:

       # :: 外部模块前缀
    ./src/main.rs   #crate 根模块(隐藏)
    ./src/a.rs      #crate::a
    ./src/b.rs      #crate::b
    ./src/c.rs  #crate::c
    ./src/c/d.rs    #crate::c::d
    

    权限规则:

    1. 模块内所有项(对外层)默认私有
    2. 即同模块的项可以访问
    3. 即父模块的项可以访问
    4. 用pub定义的项外层可以访问

    源文件定义:

    //main.rs -- crate
    mod a; //在根层即根同目录找a.rs定义
    mod b{
        pub fn f2();
    } //内联,将隐藏b.rs文件
    mod c;
    fn main(){
        a::f();
        b::f();//error。f不存在被隐藏
        c::f();//error.私有
        c::d::f(); //d 必须在c/mod.rs定义为pub mod d;
    }
    fn f(){}
    //a.rs -- crate::a
    pub fn f(){}
    fn f2(){}
    //b.rs -- crate::b
    use c::d; //use使用的永远是绝对路径,等于crate::c::d,而不是crate::b::c::d。
    fn f(){
        d::f2(); //ok
        c::d::f2(); //因为use crate::c::d;,所以导入了指定crate的模块信息到当前模块,所以c::d::f2()不会被识别为crate::b::c::d::f2()
    }
    //c.rs -- crate::c
    pub mod d;
    fn f(){
        d::f2(); //使用相对路径,等于crate::c::d::f2();
        self::d::f2(); //等于上面
    }
    //c/d.rs -- crate::c::d
    pub fn f(){
        crate::a::f();//用绝对路径::访问其他模块
        crate::a::f2();//error a不是d的父亲,d不能访问其私有成员
        crate::b::f2();//访问的是在main.rs中的定义
        super::f();//等价crate::c::f(), ok
        super::super::f();//相对路径访问main.rs 的f()
    }
    pub fn f2(){}
    

    五、错误处理

    运用策略:

    1. 默认Result
    2. 原型设计panic!
    3. 无需处理错误panic!
    4. 违反调用契约panic!

    5.1 不可恢复错误 panic!

    Cargo.toml 设置:

    [profile.release]
    panic = 'abort' #直接终止程序
    #或者展开回溯unwinding
    

    使用演示:

    panic!("error message!");
    

    5.2 可恢复错误 Result

    enum Result<T,E>{
        Ok(T),
        Err(E),
    }
    
    let f = File::open("hello.txt").expect("自动报错。"); //.unwrap();自动报错,使用内部信息
    let mut s = String::new();
    f.read_to_string(&mut s)?; //?自动向调用方返回错误对象。
    
    

    六、标准库

    6.1 集合

    6.1.1 vector

    let mut v:Vec<i64> = Vec![1,2,3];
    v.push(5);
    let el :i64 = v[1];
    let el2 :Option<i64> = v.get(1);
    for i in &mut v{
        *i += 123;
    }
    

    6.1.2 String

    StringVec<u8>的包装。

    let mut s = String::from("hello world.");
    s.push_str("man.");
    format!("{}-{}-{}", s,s,s)
    let c :char = s.chars()[1];
    let c2 :u8 = s.bytes()[2];
    let c3 :&str = &s[3..];
    

    6.1.3 map

    HashMap<K,V>

    use std::colletions::HashMap;
    let mut m= HashMap::new();
    m.insert(String::from("yes"),0);
    let v :Option<_> = m.get(&String::from("yes"));
    

    6.2 智能指针工具

    6.2.1 Box

    基本的堆引用,具有所有权。

    let a = Box::new(3);//3存放在堆上,a有所有权
    let b:i32 = *a;//解引用,取得堆上值3
    

    6.2.2 Rc

    引用计数(多所有权)。

    use std::rc::Rc;
    
    let a = Rc::new(3);
    let b = Rc::clone(&a);//引用计数+1
    let c = Rc::clone(&a);//引用计数2
    

    6.2.3 Refcell

    返回内部可变引用

    let a =Rc::new(RefCell::new(3));//创建内部可变3
    let b =Rc::clone(&a);
    let c =Rc::clone(&a);
    *a.borrow_mut()+=1; //返回内部可变引用
    

    6.2.4 Weak

    弱引用,没所有权。可用于解决循环引用问题。

    let a=Rc::new(3);
    let b:Weak<_>=Rc::downgrade(&a);//从Rc引用中创建弱引用
    drop(a); //手动删除a
    println!("b={}",b.upgrade().expect("无法访问b"));
    

    6.3 宏

    //生成自动打印内部信息的特征
    #[derive(Debug)]
    struct A(&str, i8);
    println!("{:?}", A("hi", 3));
    
    //生成等于、不等特征
    #[derive(PartialEq)] //Eq
    struct B;
    println!("B==B?{}", B==B);
    
    //生成有序特征
    #[derive(Ord,PartialOrd,Eq,PartialEq,Debug)]
    struct C(i8);
    println!("{:?}>{:?}?{}",C(3),C(1),C(3)>C(1));
    
    //生成克隆特征(要求字段实现克隆特征才有效果)
    #[derive(Clone)] //Copy
    struct D(i8);
    let mut a=D(3);
    let b = a.clone();
    let pa = a.0 as *const i8;
    let pb = b.0 as *const i8;
    println!("&{:?}->&{:?}={}",pa,pb,b.0);
    
    //生成哈希特征
    #[derive(Hash)]
    struct E;
    
    //生成默认值特征
    #[derive(Default)]
    struct F;
    

    6.4 命令行程序

    • println!
    • eprintln!

    七、并发

    并行处理,会面对如下问题:

    1. 竞争状态race conditions
    2. 死锁deadlocks
    3. 复杂的除错debug过程

    7.1 线程

    thread::spawn()

    use std::thread;
    
    let a = 3;
    let handle = thread::spawn(move||{
        println!("新线程。a={}", a);//move转移a的所有权
    });
    
    handle.join().unwrap();//主线程等待新线程执行完毕
    //否则主线程执行完毕会自动关闭子线程
    

    7.2 消息通道

    新兴的一种协调并发处理的方式。易用,单拥有权。

    use std::sync::mpsc;//多产单消multiple producer,single consumer
    use std::thread;
    
    let (tx,rx)=mpsc::channel();
    thread::spawn(move ||{tx.send("hi".to_string()).unwrap();});
    
    println!("{}",rx.recv().unwrap());//获取“hi”
    

    7.3 共享状态

    互斥器mutex(mutual exclusion):任意时刻,只允许一个线程访问数据。多拥有权。

    use std::sync::Mutex;
    use std::sync::Arc; //原子性Rc
    use std::thread;
    
    let a = Mutex::new(3);
    {
        let mut b :MutexGuard<_>= a.lock().unwrap();//获取互斥器内部数据的智能指针
        //该指针拥有锁,并能自动释放锁
        *b+=1;
    }//自动释放锁
    println!("a={:?}",a);//可再次读取数据
    
    //多线程共享互斥器
    let b = Arc::new(Mutex::new(3));//Arc是跨线程安全的Rc,即多所有权智能指针
    let c = Arc::clone(&b);//增加计数
    let handle1 = thread::spawn(move||{
        let n = c.lock().unwrap();//共享同一个锁
        *n += 1;
    });
    let d = Arc::clone(&b);//计数3
    let handle2 = thread::spawn(move||{
        let n = d.lock().unwrap();//共享同一个锁
        *n += 2;
    });
    handle1.join().unwrap();
    handle2.join().unwrap();
    println!("b={}", *b.lock().unwrap());
    

    八、自动化测试

    8.1 单元测试

    #[cfg(test)] //只在配置是test编译
    mod tests{
        use super::*; //导入全部外部模块
    
        #[test] //测试项目
        fn exploration()->Result<(),String>{
            assert!(3>2);
            panic!("测试失败。");
            Err(String::from("Result失败"))
        }
    
        #[test]
        #[should_panic] //希望出现异常,否则不通过
        fn g(){
            assert!(3<2);
        }
    }
    

    8.2 集成测试

    构建目录:/tests/,每个.rs作为独立crate构建。

    use adder; //导入待测试模块
    
    #[test]
    fn g(){
        assert!(2>3,"出错");
    }
    

    九、面向对象

    面向对象:封装并自行维护内部状态的结构体,在相同接口下多态调用。

    rust具有面向对象的设计能力,但不强制使用面向对象,因为封装是需要代价的。不要在细颗粒度下使用面向对象,避免过度包装。

    多态技术 动态性 同构性
    范型 静态 同构
    特征对象 动态 异构
    • 静态:编译期确定真实调用
    • 异构:构造的复合结构的类型可以不相同

    面向对象接口设计建议:

    1. 被动方,需要改动自身状态的
    2. 封装自身状态必须的输入输出过滤
    3. 思考单对象,双对象,多对象下的设计差异

    十、命令行接口cli

    一些命令行下的编译管理工具:

    • cargo : 项目管理工具
      • new :创建项目
      • build :构建
      • publish :发布到网站
      • login :登录crates.io 共享网站
      • test :运行测试
      • install :从网站下载程序
      • run :运行
    • rustc : 编译器
  • 相关阅读:
    python 获取Excel 的内容
    python 获取文件Excel 的行数与列数
    python 读取Excel 取出表头(列名)
    DRF的视图组件
    Redis
    Git的故事
    DRF的JWT用户认证
    DRF的三大认证组件
    DRF的序列化组件
    DRF的请求响应组件
  • 原文地址:https://www.cnblogs.com/Nobel/p/11481702.html
Copyright © 2011-2022 走看看