• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Rust 编程:道-类型系统

武飞扬头像
IPrivacy
帮助1

类型系统

基本类型

一切皆表达式,表达式皆有值,值皆有类型,所以一切皆类型。
包括:

  • 基本类型
  • 复合类型
  • 作用域
  • 返回值
    • Option<T>
    • Result<T,E>

编程语言中不同的类型本质上是内存占用空间和编码方式的不同,Rust也不例外。

Rust中绝大部分类型都是在编译期可确定大小的类型(Sized Type),如u8,f64等

Rust也有少量的动态大小的类型(Dynamic Sized Type,DST),如str

对于动态类型,Rust提供了引用方式来解决,引用类型存在栈空间,具体内容存在堆空间。

例如:

学新通

包含了动态大小类型地址信息和携带了长度信息的指针,叫作胖指针(Fat Pointer),&str就是胖指针。

有如下代码

fn test(mut arr:[u32]){
	arr[1] = 5;
	arr[2] = 1;
}
fn main(){
	let arr:[u32]= [1,2,4,5,6];
	test(arr);
}

以上代码编译会出错

学新通

表示期望切片类型,找到的是数组类型,做如下修改可通过编译

fn test(mut arr:[u32;5]){
	arr[1] = 5;
	arr[2] = 1;
}
fn main(){
	let arr:[u32;5]= [1,2,4,5,6];
	test(arr);
    println!("{:?}",arr);// 1 2 4 5 6
}

这样类型就匹配了,但是arr结果并没有被改变,其实是因为传入的参数通过shadow机制在栈空间创建了一个新的arr,所以并不会改变原来的值。可以类比C语言的值传递swap(a,b),结果并不会交换a,b。

c语言解决方法是传入a,b的指针,Rust也一样,传入可变引用,即胖指针即可

fn reverse(arr:&mut [u32]){
    arr[0] = 3;
	arr[1] = 2;
	arr[2] = 1;
    println!("len : {}",arr.len());
}
fn main(){
	let mut arr= [1,2,3];//[u32;3]
	reverse(&mut arr);//传入的引用包含了大小,编译器解决
    println!("{:?}",arr);
}

可以查看占用内容大小

fn main(){
    println!("{}",std::mem::size_of::<&mut [u32]>());//16
    println!("{}",std::mem::size_of::<& [u32;3]>());//8
}

胖指针由来。

ZST类型

除了可确定大小类型和DST类型,Rust还支持零大小类型(Zero Sized Type,ZST),比如单元类型和单元结构体,大小都是零。

以下都为ZST类型

enum void {}
struct foo;
struct Baz{
    f:foo,
    q:(),
    buf:[u16;0],
    em:void,
}
fn main(){
    println!("{}",std::mem::size_of::<void>());//0
    println!("{}",std::mem::size_of::<foo>());//0
    println!("{}",std::mem::size_of::<Baz>());//0
    println!("{}",std::mem::size_of::<[();32]>());//0
}

底类型

如never类

zst是空,底类型是无。

  • 没有值。
  • 是其他任意类型的子类型
  • 用!表示

以下返回底类型:

  • 发散函数(Diverging Function)
  • continue和break关键字
  • loop循环
  • 空枚举,比如enum Void{}

以下代码正常执行

fn foo()->!{
    loop { }
}
fn main(){
    let a = if false{
        foo()
    }else{
        100
    };
    print!("{}",a);
}

因为!是所有类型的子类型。

以下不可

enum Void {}
fn main(){
    let  res:Result<u32,Void> = Ok(0);
    let Ok(num) = res;
}

Rust中使用Result类型来进行错误处理,强制开发者处理Ok和Err两种情况,但是有时可能永远没有Err,这时使用enum Void{}就可以避免处理Err的情况。

但是可惜的是,当前版本的Rust还不支持上面的语法,编译会报错。不过Rust团队还在持续完善中,在不久的将来Rust就会支持此用法。

类型推断

Rust支持类型推断,但其功能并不像Haskell那样强大,Rust只能在局部范围内进行类型推导

fn add(a:u32,b:u32)->u32{
    a b
}

fn main(){
    let a = 1;
    let b = 2;
    add(a, b);
}

Turbofish操作符

fn main(){
   let x = "1";
   print!("{:?}",x.parse::<i32>().unwrap());
     
}

::<>就是Turbofish操作符

不完善

fn main(){
   let x = 0;
   let flag = x.is_positive();
}

can’t call method is_positive on ambiguous numeric type {integer}

泛型

泛型(Generic)是一种参数化多态。使用泛型可以编写更为抽象的代码,减少工作量。简单来说,泛型就是把一个泛化的类型作为参数,单个类型就可以抽象化为一簇类型。

即把类型作为参数。

泛型可以用在,函数,结构体,使用泛型必须声明<T>

struct Point<T>{
    x:T,
    y:T,
}
fn get_x<T>(x:T) -> T{
    x
} 

为泛型结构体实现方法impl<T>

struct Message<T>{
    content:T,
}
impl <T> Message<T> {
    fn new(content:T)->Self{
        Message { content }
    }
}

Rust中的泛型属于静多态,它是一种编译期多态。在编译期,不管是泛型枚举,还是泛型函数和泛型结构体,都会被单态化

(Monomorphization)。单态化是编译器进行静态分发的一种策略

泛型及单态化是Rust的最重要的两个功能。单态化静态分发的好处是性能好,没有运行时开销;缺点是容易造成编译后生成的二进制文件膨胀。这个缺点并不影响使用Rust编程。

返回值推导

struct Foo(i32);
struct Bar(i32,i32);

trait Instance {
    fn new(i:i32) -> Self;
}

impl Instance for Foo {
    fn new(i:i32) -> Foo {
        Foo(i)
    }
}
impl Instance for Bar{
    fn new(i:i32)->Self{//Self指结构体本身
        Bar(i,i 10)
    }
}
fn foobar<T:Instance>(i:i32) -> T{//表明T是实现了Instance trait的结构体
    T::new(i)
}

fn main(){
    let f:Foo = foobar(10);
    let b:Bar = foobar(20);
}
学新通

深入Trait

可以说trait是Rust的灵魂。Rust中所有的抽象,比如接口抽象、OOP范式抽象、函数式范式抽象等,均基于trait来完成。同时,trait也保证了这些抽象几乎都是运行时零开销的。

从类型系统的角度来说,trait是Rust对Ad-hoc多态的支持。从语义上来说,trait是在行为上对类型的约束,这种约束可以让trait有如下4种用法:

  • 接口抽象。对类型行为的统一约束。
  • 泛型约束。泛型的行为被trait限定在更有限的范围内。
  • 抽象类型。在运行时作为一种间接的抽象类型去使用,动态地分发给具体的类型。
  • 标签trait。对类型的约束,直接作为标签使用。

接口抽象

特点:

  • 接口中定义方法,并支持默认实现。

  • 接口中不能实现另一个接口,但接口间可以继承。

  • 同一个接口可以被多个类型实现,但不能被同一个类型实现。

  • 使用impl关键字为类型实现接口方法。

  • 使用trait关键字定义接口。

    学新通

trait T_A{
    fn say(msg:&str)->String;
}
struct A;
struct B;
enum C{
}

impl T_A for A{
    fn say(msg:&str)-> String{
        let t = "A".to_string();
        t   msg
    }
}
impl T_A for B{
    fn say(msg:&str)->String{
        let t = "B".to_string();
        t   msg
    }
}
impl T_A for C{
    fn say(msg:&str)->String{
        let t = "C".to_string();
        t   msg
    }
}


fn main(){

}
学新通

加减乘除等也是一种trait

trait Add<RHS,Output>{
    fn my_add(self,rhs:RHS) -> Output;
}
impl Add<i32,i32> for i32 {
    fn my_add(self,rhs:i32)->i32{
        self   rhs
    }
}

impl Add<u32,i32> for u32{
    fn my_add(self,rhs:u32) -> i32{
        (self   rhs) as i32
    }
}

fn main(){
    let (a,b,c,d) = (1i32,2i32,3u32,4u32);
    let x:i32 = a.my_add(b);
    let y:i32 = c.my_add(d);
    assert_eq!(x,3i32);
    assert_eq!(y,7i32);
}
学新通

RHS是 的右侧值类型,Output是输出类型。

对于Add,加法的输入与输出值应该为同一类型,所以Add<RHS,Output>的output有点多余,于是有以下写法

pub trait Add<RHS =Self >{// 指定RHS的默认值为Self
    type Output;
    fn add(self,rhs:RHS) -> Self::Output;
}

type Output 叫关联类型,Self是每个trait都带有的隐式类型参数。

impl Add<&str> for String{
    type Output = String;
    fn add(mut self,other:&str) -> String{
        self.push_str(other);
        self
    }
}

trait一致性

可以实现操作符重载。

如想要实现u32 u64

use std::ops::Add;
impl Add<u64> for u32{
    type Output = u64;
    fn add(self,other:u64) -> Self::Output{
        (self as u64)   other
    }
}
 

fn main(){
    let a = 1u32;
    let b = 2u64;
    println!("{}",a b);
}

编译报错。

学新通

这是因为Rust遵循一条重要的规则:孤儿规则(Orphan Rule)。孤儿规则规定,如果要实现某个trait,那么该trait和要实现该trait的那个类型至少有一个要在当前crate中定义

Add trait和u32、u64都不是在当前crate中定义的,而是定义于标准库中的。如果没有孤儿规则的限制,标准库中u32类型的加法行为就会被破坏性地改写,导致所有使用u32类型的crate可能产生难以预料的Bug。

解决如下,讲Add trait 定义在当前crate就可以了,当然可以不一定叫Add,和add()

 trait Add<RHS =Self >{
    type Output;
    fn add(self,rhs:RHS) -> Self::Output;
}
impl Add<u64> for u32{
    type Output = u64;
    fn add(self,other:u64) -> Self::Output{
        (self as u64)   other
    }
}

fn main(){
    let a = 1u32;
    let b = 2u64;
    println!("{}",a.add(b));
}
学新通

对其他类型实现trait

 use std::ops::Add;
#[derive(Debug)]
 struct Point{
     x:i32,
     y:i32,
 }
 impl Add for Point  {
     type Output = Self;
     fn add(self,other:Point) -> Self::Output{
         Point{
             x:self.x other.x,
             y:self.y other.y,
         }
     }
 }

fn main(){
    let p1 = Point{
        x:1,
        y:2,
    };
    let p2 = Point{
        x:2,
        y:3,
    };
    println!("{:?}",p1 p2);
}
学新通

trait 继承

Rust不支持传统面向对象的继承,但是支持trait继承。子trait可以继承父trait中定义或实现的方法。在日常编程中,trait中定义的一些行为可能会有重复的情况,使用trait继承可以简化编程,方便组合,让代码更加优美。

例如web中常用的分页

trait Page{
    fn set_page(&self, p:i32){
        println!("Page Default 1");
    }
}
trait PerPage{
    fn set_perpage(&self, num:i32){
        println!("Per Page Default 10");
    }
}
struct MyPaginate{page:i32}
impl Page for MyPaginate {}
impl PerPage for MyPaginate {}
fn main(){
    let my_paginate = MyPaginate{page:1};
    my_paginate.set_page(2);
    my_paginate.set_perpage(100);
}
学新通

MyPaginate实现了两个trait。

如果要再实现一个跳转功能,可以使用继承

trait Paginate: Page   PerPage{//继承写法 :trait
    fn set_skip_page(&self,num:i32){
        println!("skip to page {}",num);
    }
}
impl <T:Page PerPage> Paginate for T {}//T为泛型,类型为实现了Page PerPage的类型,整句话表示为实现了T的类型实现Paginate
fn main(){
    let my_paginate = MyPaginate{page:1};
    my_paginate.set_page(2);
    my_paginate.set_perpage(100);
    my_paginate.set_skip_page(12);//skip to page 12
}

优点是添加了新功能再不影响之前功能的情况下。

trait约束

很多情况下,一个行为并不是为所有类型实现的。比如:

fn sum<T>(a:T,b:T)->T{//编译不通过
    a b
}

整型相加可以,字符串相加可以,但是整型与bool类型,就不行。

可以限制T为可加类型

use std::ops::Add;
fn sum<T:Add<T,Output=T>>(a:T,b:T)->T{//限制T的类型为实现了Add的类型
    a b
}

fn main(){
    let a = 1;
    let b = 2;
    let c = sum(a, b);
    println!("{}",c);
}

使用trait对泛型进行约束,叫作trait限定(trait Bound)。格式如下

fn generic<T: MyTrait   MyOtherTrait   SomeStandTrait>(t:T){}

泛型限定是许多语言都有概念,是Structural Typing的变种,Rust中的trait限定也是Structural Typing的一种实现。

也可以从数学的角度理解trait限定,例如

trait Paginate: Page   PerPage{}

注意:

如果有trait A,B,C,A,B,C中的方法不能同名,

不能覆盖,

C:A B,为某类型实现C必须要实现A和B。否则报错。

trait A {
    fn getA(&self,i:i32){
        println!("A : {}",i);
    }
}
trait B {
    fn getB(&self,i:i32){
        println!("B : {}",i);
    }
}
trait C:A B {
    fn getC(&self,i:i32){
        println!("C : {}",i);
    }
}
impl A for Test {}
impl B for Test {}
// impl  C for Test {}
impl <T> C for T
where T:A B
{} 
struct Test{
    a:i32,
}
 
 
fn main(){
    let test =  Test{a:1};
    test.getB(2);
    test.getA(1);
    test.getC(3);
  
}
学新通

抽象类型

相对于具体类型而言,抽象类型无法直接实例化,它的每个实例都是具体类型的实例。

对于抽象类型而言,编译器可能无法确定其确切的功能和所占的空间大小。所以Rust目前有两种方法来处理抽象类型:trait对象和impl Trait。

trait对象

use std::fmt::Debug;

 
#[derive(Debug)]
struct Foo;

#[derive(Debug)]
struct  Fun;

trait Bar{
    fn baz(&self);
}
impl Bar for Foo{
    fn baz(&self){
        println!("{:#?}",self);
    }
}
impl Bar for Fun{
    fn baz(&self){
        println!("{:#?}",self);
    }
}
fn static_dispatch<T>(t:&T)
where T:Bar{
    t.baz();
}
fn dynamic_dispatch(t:&dyn Bar){//动态分发
    t.baz();
}
fn main(){
    let foo = Foo;
    let fun = Fun;
    static_dispatch(&foo);
    static_dispatch(&fun);
    dynamic_dispatch(&foo);
    dynamic_dispatch(&fun);
}
学新通

动态分发

trait本身也是一种类型,但它的类型大小在编译期是无法确定的,所以trait对象必须使用指针。可以利用引用操作符&或 Box<T>来制造一个 trait 对象。trait 对象等价代码如下结构体

pub struct TraitObject{
    pub data: *mut (),
    pub vtable: *mut (),
} 

TraitObject 在栈区,数据指针指向堆区数据部分,vtable 名称来自C ,保护了析构函数,大小,函数等信息。

学新通

在编译器TraitObject不知道调用哪个方法,但是指针的大小确定,在运行器通过trait_object.method()可以知道函数的指针,然后进行调用。

类比java继承父类可以调用子类的实现。

并不是每个trait都可以作为trait对象被使用,这依旧和类型大小是否确定有关系。每个trait都包含一个隐式的类型参数Self,代表实现该trait的类型。Self默认有一个隐式的trait限定?Sized,形如<Self:?Sized>,?Sized trait 包括了所有的动态大小类型和所有可确定大小的类型。Rust 中大部分类型都默认是可确定大小的类型,也就是<T:Sized>,这也是泛型代码可以正常编译的原因。

必须同时满足以下两条规则的trait才可以作为trait对象使用

· trait的Self类型参数不能被限定为Sized。
· trait中所有的方法都必须是对象安全的。

简单来说,要将trait作为trait对象就不加限制,否则就加上Sized

trait A:Sized{
 //code
}

安全的trait对象实例

 trait Bar{
    fn bax(self,x:u32);
    fn baz(&mut self);
 }

不安全的trait对象实例

trait Foo{
     fn bad<T>(&self,x:T);
     fn new() ->Self;//Self是unsized
 }
  1. 拆分
 trait Foo{
     fn bad<T>(&self,x:T);
 }
 trait Foo:Bar{
    fn new() ->Self;//Self是Bar继承的
}
  1. 使用where限定
trait Foo{
    fn bad<T>(&self,x:T);
    fn new()->Self where Self:Sized;
}

impl Trait

在Rust 2018版本中,引入了可以静态分发的抽象类型impl Trait。如果说trait对象是装箱抽象类型(Boxed Abstract Type)的话,那么impl Trait就是拆箱抽象类型(Unboxed Abstract Type)。“装箱”和“拆箱”是业界的抽象俗语,其中“装箱”代表将值托管到堆内存,而“拆箱”则是在栈内存中生成新的值,目前impl Trait只可以在输入的参数和返回值这两个位置使用,在不远的将来,还会拓展到其他位置,比如let定义、关联类型等.

例如

use std::fmt::Debug;


pub trait Fly {
    fn fly(&self) ->bool;
}

#[derive(Debug)]
struct Duck;

#[derive(Debug)]
struct Pig;

impl Fly for Duck {
    fn fly(&self)->bool{
        return true;
    }
}
impl Fly for Pig{
    fn fly(&self) -> bool{
        return false;
    }
}

fn fly_static(s: impl Fly Debug) ->bool{
    s.fly()
}
fn can_fly(s:impl Fly Debug) -> impl Fly{
    if s.fly(){
        println!("{:?} can fly",s);
    }else {
        println!("{:?} can't fly",s);
    }
    s
}
fn main() {
    let pig = Pig;
    assert_eq!(fly_static(pig),false);
    let pig = Pig;
    let pig = can_fly(pig);
    let duck = Duck;
    assert_eq!(fly_static(duck),true);
    let duck = Duck;
    let duck = can_fly(duck);
}
学新通

将impl Trait语法用于参数位置的时候,等价于使用trait限定的泛型。

let a: impl Trait 是不允许的。

另外,impl Trait只能用于为单个参数指定抽象类型,如果对多个参数使用implTrait语法,编译器将报错

use std::ops::Add;
fn sum<T>(a:impl Add<Output = T>,b:impl Add<Output = T>)->T{
    a   b
}

impl 与 dyn是对应的,一个静态,一个动态。

标签trait

trait 这种对行为约束的特性也非常适合作为类型的标签。

Rust一共提供了5个重要的标签trait,都被定义在标准库std::marker模块中,分别为:

· Sized trait,用来标识编译期可确定大小的类型。
· Unsize trait,目前该trait为实验特性,用于标识动态大小类型(DST)。
· Copy trait,用来标识可以按位复制其值的类型。
· Send trait,用来标识可以跨线程安全通信的类型。
· Sync trait,用来标识可以在线程间安全共享引用的类型。

#[stable(feature = "rust1", since = "1.0.0")]
#[lang = "sized"]
pub trait Sized {
    // Empty.
}

这里真正起“打标签”作用的是属性#[lang="sized"],该属性lang表示Sized trait供Rust语言本身使用,声明为"sized",称为语言项(Lang Item),这样编译器就知道Sized trait如何定义了。

默认为Sized,否则

struct Bar<T:?Sized>(T);

Copy trait 继承Clone

#[lang = "copy"]
pub trait Copy: Clone {
    // Empty.
}
#[lang = "clone"]
pub trait Clone: Sized {
    fn clone(&self) -> Self;
    #[inline]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn clone_from(&mut self, source: &Self) {
        *self = source.clone()
    }
}

Rust为很多基本数据类型实现了Copy trait,比如常用的数字类型、字符(Char)、布尔类型、单元值、不可变引用等

Rust提供了Send和Sync两个标签trait,它们是Rust无数据竞争并发的基石。

类型转换

在编程语言中,类型转换分为隐式类型转换(Implicit Type Conversion)和显式类型转换(Explicit Type Conversion)。隐式类型转换是由编译器或解释器来完成的,开发者并未参与,所以又称之为强制类型转换(Type Coercion)。显式类型转换是由开发者指定的,就是一般意义上的类型转换(Type Cast)。

Deref解引用

Rust中的隐式类型转换基本上只有自动解引用。自动解引用的目的主要是方便开发者使用智能指针。Rust 中提供的 Box<T>、Rc<T>和 String 等类型,实际上是一种智能指针。

引用符&,解引用*

解引用可以自己实现,只要实现了Deref Trait 即可实现类型转换。

Deref的定义,

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

DerefMut,返回的是可变应用。

解引用例子,字符串连接

fn main(){
    let s1 = "12".to_string();
    let s2 = "34".to_string();
    let s3 = s1   &s2;
    println!("{}",s3);

}

s1,s2都是String类型,&s2应该是&String,期望的是&str,也就是说应该会报错,可是以上代码正常执行,就是因为String实现了解引用,

impl Deref for String {
    type Target = str;
    fn deref(&self) -> &str{
        unsafe{str::from_utf8_unchecked(&self.vec)}
    }
}

标准库中常用的其他类型都实现了Deref,比如Vec<T>、Box<T>、Rc<T>、Arc<T>等。实现Deref的目的只有一个,就是简化编程。

解引用实例

use std::rc::Rc;
fn test_vec(s:&[i32]){
    println!("{:?}",s);
}

fn test_rc(){
    let x = Rc::new("hello");
    println!("{}",x);
}
fn main(){
   let v = vec![1,2,3];
   test_vec(&v);
   test_rc();
}

手动解引用

当某类型和其解引用目标类型中包含了相同的方法时,编译器就不知道该用哪一个了。此时就需要手动解引用,

fn test_deref(){
    let x = Rc::new("hello");
    let y = x.clone();//&Rc<&str>
    let z = (*x).clone();//&str
}

因为Rc和str都实现了clone,所以不能自动解引用。

match需要手动解引用

fn test_str(){
    let s = "1234".to_string();
    match &s[..]{
        "1234" => println!("hello"),
        _ => {}
    }
}

有如下几种方式:

  • match x.deref()
  • match x.as_ref()
  • match x.borrow()
  • match &*x
  • match &x[…]

as 操作

as 操作符最常用的场景就是转换 Rust 中的基本数据类型。需要注意的是,as 关键字不支持重载。

fn main(){
    let a = 1u32;
    let b = a as u64;
    let c = 3i64;
    let d = c as u32;
    print!("a{},b{},c{},d{}",a,b,c,d);
}

长类型转短类型截断。

fn main(){
   let a = u32::MAX;
   let b = a as u16;
   println!("a:{},b:{}",a,b);//a:4294967295,b:65535
}

as 还可以消除语法歧义。

#[derive(Debug)]
struct C;
trait A {
    fn test(&self);
} 
trait B {
    fn test(&self);
}
impl A for C {
    fn test(&self){
        println!("A:{:?}",self);
    }
}
impl B for C {
    fn test(&self){
        println!("B:{:?}",self);
    }
}

fn main(){
   let c = C;
    A::test(&c);
    B::test(&c);
    <C as A>::test(&c);
    <C as B>::test(&c);
}
学新通

类型和子类型相互转换

as转换还可以用于类型和子类型之间的转换。Rust中没有标准定义中的子类型,比如结构体继承之类,但是生命周期标记可看作子类型。比如&'static str类型是&'a str类型的子类型,因为二者的生命周期标记不同,'a 和'static 都是生命周期标记,其中'a 是泛型标记,是&str的通用形式,而'static则是特指静态生命周期的&str字符串。

fn main(){   
    let a:&'static str = "hello";
    let b:& str = a as &str;
    let c:&'static str = b as &'static str;
}

From和Into

From和Into是定义于std::convert模块中的两个trait。它们定义了from和into两个方法,这两个方法互为反操作。

定义

pub trait From<T>: Sized {
    /// Performs the conversion.
    #[lang = "from"]  
    fn from(_: T) -> Self;
}
pub trait Into<T>: Sized {
    /// Performs the conversion.
    #[must_use]
    fn into(self) -> T;
}

关于Into有一条默认的规则:如果类型U实现了From<T>,则T类型实例调用into方法就可以转换为类型U

因为Rust实现了

impl<T,U> Into<U> for T where U:From<T>

当前trait系统的不足

· 孤儿规则的局限性。
· 代码复用的效率不高。
· 抽象表达能力有待改进。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgkkgjf
系列文章
更多 icon
同类精品
更多 icon
继续加载