Loading

Rust 泛型、Trait 和生命周期

本文在原文基础上有删减,原文参考泛型、Trait 和生命周期

泛型数据类型

可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。

在函数定义中使用泛型

当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示

两个函数都是寻找 slice 中最大值,不同点只是名称和签名类型:

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
}

为了定义泛型版本的 largest 函数,类型参数声明位于函数名称与参数列表中间的尖括号 <> 中,像这样:

fn largest<T>(list: &[T]) -> &T {

一个使用泛型参数的 largest 函数定义:

//函数 largest 有泛型类型 T,
//有个参数 list,其类型是元素为 T 的 slice
//largest 函数会返回一个与 T 相同类型的引用
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    //标准库为 i32 和 char 实现了 PartialOrd

    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

为了开启比较功能,标准库中定义的 std::cmp::PartialOrd trait 可以实现类型的比较功能,限制 T 只对实现了 PartialOrd 的类型有效否则代码会编译错误。

结构体定义中的泛型

可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。
Point 结构体存放了两个 T 类型的值 x 和 y:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

字段 x 和 y 的类型必须相同,因为它们都有相同的泛型类型 T:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    //代码不能编译
    let wont_work = Point { x: 5, y: 4.0 };
}

使用两个泛型的 Point,x 和 y 可以是不同类型

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。当代码中需要很多泛型时,这表明代码需要重构分解成更小的结构。

枚举定义中的泛型

枚举也可以在成员中存放泛型数据类型,如 Option<T> 枚举:

enum Option<T> {
    Some(T),
    None,
}

枚举也可以拥有多个泛型类型,如 Result 枚举:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result 有两个成员:Ok,它存放一个类型 T 的值,而 Err 则存放一个类型 E 的值。

方法定义中的泛型

在为结构体和枚举实现方法时,一样也可以用泛型。在 Point<T> 结构体上实现方法 x,它返回 T 类型的字段 x 的引用:

struct Point<T> {
    x: T,
    y: T,
}
//必须在 impl 后面声明 T,可以选择不同的名称
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

定义方法时也可以为泛型指定限制(constraint),构建一个只用于拥有泛型参数 T 的结构体的具体类型的 impl 块:

//其他 T 不是 f32 类型的 Point<T> 实例没有定义此方法
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型,以下方法使用了与结构体定义中不同类型的泛型:

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}
//用 self 的 Point 类型的 x 值(类型 X1)和参数的 Point 类型的 y 值(类型 Y2)来创建一个新 Point 类型的实例
impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    //p3拥有一个 i32 类型的 x,因为 x 来自 p1
    //p3拥有一个 char 类型的 y,因为 y 来自 p2
    let p3 = p1.mixup(p2);

    //会打印出 p3.x = 5, p3.y = c
    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

泛型代码的性能

Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。在这个过程中,编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码

标准库中的 Option 枚举:

let integer = Some(5);
let float = Some(5.0);

编译器生成的单态化版本的代码类如下(命名可能不同):

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

Rust 会将每种情况下的泛型代码编译为具体类型,使用泛型没有运行时开销。、

Trait:定义共同行为

trait 定义了某个特定类型拥有可能与其他类型共享的功能,可以通过 trait 以一种抽象的方式定义共同行为,可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

注意:trait 类似于其他语言中的常被称为 接口(interfaces)的功能,虽然有一些不同。

定义 trait

Summary trait 定义,它包含由 summarize 方法提供的行为:

//声明 trait 为 pub 以便依赖这个 crate 的 crate 也可以使用这个 trait
pub trait Summary {
    //声明描述实现这个 trait 的类型所需要的行为的方法签名
    fn summarize(&self) -> String;
}

trait 体中可以有多个方法:一行一个方法签名且都以分号结尾

为类型实现 trait

在 NewsArticle 和 Tweet 类型上实现 Summary trait:

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        //使用标题、作者和创建的位置作为返回值
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        //用户名后跟推文的全部文本作为返回值
        format!("{}: {}", self.username, self.content)
    }
}

trait 必须和类型一起引入作用域以便使用额外的 trait 方法:

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        // 初始化 Tweet 实例...
    };

    println!("1 new tweet: {}", tweet.summarize());
}

需要注意的限制是,只有在 trait 或类型至少有一个属于当前 crate 时才能对类型实现该 trait,不能为外部类型实现外部 trait。
这个限制是被称为 相干性(coherence)的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。

默认实现

有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。
Summary trait 的定义,带有一个 summarize 方法的默认实现:

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

想要对 NewsArticle 实例使用这个默认实现,可以通过 impl Summary for NewsArticle {} 指定一个空的 impl 块,然后对 NewsArticle 实例调用 summarize 方法:

let article = NewsArticle {
    // 初始化 NewsArticle 实例...
};
//会打印 New article available! (Read more...)
println!("New article available! {}", article.summarize());

默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现,重新定义 Summary trait:

pub trait Summary {
    //一个需要实现的 summarize_author 方法
    fn summarize_author(&self) -> String;
    //此方法的默认实现调用 summarize_author 方法
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

只需在实现 trait 时定义 summarize_author 即可:

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

一旦定义了 summarize_author 就可以对 Tweet 结构体的实例调用 summarize 了:

let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from(
        "of course, as you probably already know, people",
    ),
    reply: false,
    retweet: false,
};
//会打印出 1 new tweet: (Read more from @horse_ebooks...)
println!("1 new tweet: {}", tweet.summarize());

trait 作为参数

定义一个函数 notify 来调用其参数 item 上的 summarize 方法,该参数是实现了 Summary trait 的某种类型:

//使用 impl Trait 语法
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

item 参数指定了 impl 关键字和 trait 名称,而不是具体的类型,该参数支持任何实现了指定 trait 的类型

Trait Bound 语法

impl Trait 语法实际上是一种较长形式语法的语法糖,称为 trait bound

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

impl Trait 适用于短小的例子,更长的 trait bound 则适用于更复杂的场景:

  • item1 和 item2 允许是不同类型的情况:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
  • item1 和 item2 值的具体类型必须一致的情况:
pub fn notify<T: Summary>(item1: &T, item2: &T) {

通过 + 指定多个 trait bound

如果 item 需要同时实现两个不同的 trait:Display 和 Summary,可以通过 + 语法实现:

pub fn notify(item: &(impl Summary + Display)) {
  • 语法也适用于泛型的 trait bound:
pub fn notify<T: Summary + Display>(item: &T) {

通过指定这两个 trait bound,notify 的函数体可以调用 summarize 并使用 {} 来格式化 item。

通过 where 简化 trait bound

过多的 trait bound 使得函数签名难以阅读:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

Rust 有另一个在函数签名之后的 where 从句中指定 trait bound 的语法:

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{

返回实现了 trait 的类型

在返回值中使用 impl Trait 语法来返回实现了某个 trait 的类型:

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

不过这只适用于返回单一类型的情况,尝试返回 NewsArticle 或 Tweet 会编译错误:

fn returns_summarizable(switch: bool) -> Summarizable {
    if switch {
        Summarizable::NewsArticle(NewsArticle {
            // 初始化 NewsArticle 实例...
        })
    } else {
        Summarizable::Tweet(Tweet {
            // 初始化 Tweet 实例...
        })
    }
}

使用 trait bound 有条件地实现方法

通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。
根据 trait bound 在泛型上有条件的实现方法:

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}
//只有那些为 T 类型实现了 PartialOrd trait(来允许比较) 和 Display trait(来启用打印)的 Pair<T> 才会实现 cmp_display 方法
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations,如标准库为任何实现了 Display trait 的类型实现了 ToString trait:

impl<T: Display> ToString for T {
    // --snip--
    // to_string 方法
}
```rust
整型实现了 Display:
```rust
let s = 3.to_string();

生命周期确保引用有效

Rust 中的每一个引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明它们的关系。

生命周期避免了悬垂引用

生命周期的主要目标是避免悬垂引用(dangling references),后者会导致程序引用了非预期引用的数据。
尝试使用离开作用域的值的引用,代码会编译错误:

fn main() {
    let r;
    //如果尝试在给它一个值之前使用这个变量会出现一个编译时错误,Rust 不允许空值。

    {
        let x = 5;
        r = &x;
    }
    
    // 编译错误,r 引用的值在尝试使用之前就离开了作用域
    println!("r: {}", r);
}

借用检查器

Rust 编译器有一个 借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。

r 和 x 的生命周期注解,分别叫做 'a 和 'b:

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,不过它引用了一个拥有生命周期 'b 的对象。程序被拒绝编译,因为生命周期 'b 比生命周期 'a 要小:被引用的对象比它的引用者存在的时间更短

一个有效的引用,因为数据比引用有着更长的生命周期:

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

这里 x 拥有生命周期 'b,比 'a 要大,这就意味着 r 可以引用 x:Rust 知道 r 中的引用在 x 有效的时候也总是有效的。

函数中的泛型生命周期

main 函数调用 longest 函数来寻找两个字符串 slice 中较长的一个:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    //会打印出 The longest string is abcd
    println!("The longest string is {}", result);
}

实现 longest 函数的错误示例,它并不能编译:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 x 或 y。

生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短,相反它们描述了多个引用生命周期相互的关系,而不影响其生命周期。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用

生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短。生命周期参数注解位于引用的 & 之后,并有一个空格来将引用类型与生命周期注解分隔开。

大多数人使用 'a 作为第一个生命周期注解:

&i32        // 引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

函数签名中的生命周期注解

为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期(lifetime)参数,就像泛型类型(type)参数一样。

longest 函数定义指定了签名中所有的引用必须有相同的生命周期 'a,也就是这两个参数和返回的引用存活的一样久:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

记住通过在函数签名中指定生命周期参数时,并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝
注意: longest 函数并不需要知道 x 和 y 具体会存在多久,而只需要知道有某个可以被 'a 替代的作用域将会满足这个签名

当具体的引用被传递给 longest 时,泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个
通过拥有不同的具体生命周期的 String 值调用 longest 函数:

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        //能够编译和运行,并打印出 The longest string is long string is long
        println!("The longest string is {}", result);
    }
}

尝试在 string2 离开作用域之后使用 result,不能编译:

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    //编译错误,result 的引用的生命周期必须是两个参数中较短的那个
    println!("The longest string is {}", result);
}

深入理解生命周期

指定生命周期参数的正确方式依赖函数实现的具体功能,如下代码将能够编译:

//y 的生命周期与参数 x 和返回值的生命周期没有任何关系,不用指定
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。
一个不能编译的 longest 函数实现:

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    //编译错误:result 在 longest 函数的结尾将离开作用域并被清理
    result.as_str()
}

在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。

生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的,一旦它们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

结构体定义中的生命周期注解

可以定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解
一个存放引用的结构体,所以其定义需要生命周期注解:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

这个注解意味着 ImportantExcerpt 的实例不能比其 part 字段中的引用存在的更久

生命周期省略(Lifetime Elision)

定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用,以下代码能编译成功:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

在早期版本,每一个引用都必须有明确的生命周期,接着 Rust 团队把一些可预测的模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。未来只会需要更少的生命周期注解

被编码进 Rust 引用分析的模式被称为 生命周期省略规则(lifetime elision rules),并不是需要程序员遵守的规则,而是一系列特定的场景:

  • 如果代码符合这些场景,就无需明确指定生命周期。
  • 如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,编译器会给出一个错误提示。

函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)

编译器采用三条规则来判断引用何时不需要明确的注解,第一条规则适用于输入生命周期,后两条规则适用于输出生命周期,这些规则适用于 fn 定义,以及 impl 块:

  • 第一条规则是编译器为每一个引用参数都分配一个生命周期参数。

    • 函数有一个引用参数的就有一个生命周期参数:fn foo<'a>(x: &'a i32)
    • 有两个引用参数的函数就有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
    • 依此类推
  • 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

  • 第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法 (method),那么所有输出生命周期参数被赋予 self 的生命周期。

应用这些规则来计算上面示例中 first_word 函数签名中的引用的生命周期:

//开始时签名中的引用并没有关联任何生命周期
fn first_word(s: &str) -> &str {
//接着编译器应用第一条规则,签名看起来像这样
fn first_word<'a>(s: &'a str) -> &str {
//第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,签名看起来像这样
fn first_word<'a>(s: &'a str) -> &'a str {
//现在这个函数签名中的所有引用都有了生命周期

再看看另一个例子,没有生命周期参数的 longest 函数:

fn longest(x: &str, y: &str) -> &str {
//第一条规则:有两个参数就有两个(不同的)生命周期
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
//第二条规则:函数存在多个输入生命周期,不适用
//第三条规则:没有 self 参数,不适用
//编译出现错误:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。

方法定义中的生命周期注解

(实现方法时)结构体字段的生命周期必须总是在 impl 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。

方法 level 唯一的参数是 self 的引用,而且返回值只是一个 i32,并不引用任何值:

//impl 之后和类型名称之后的生命周期参数是必要的
impl<'a> ImportantExcerpt<'a> {
    //因为第一条生命周期规则,不是必须标注 self 引用的生命周期
    fn level(&self) -> i32 {
        3
    }
}

一个适用于第三条生命周期省略规则的例子:

impl<'a> ImportantExcerpt<'a> {
    //Rust 应用第一条生命周期省略规则并给予 &self 和 announcement 它们各自的生命周期
    //其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

静态生命周期

有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间,所有的字符串字面值都拥有 'static 生命周期:

let s: &'static str = "I have a static lifetime.";

这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的,因此所有的字符串字面值都是 'static 的。

结合泛型类型参数、trait bounds 和生命周期

简要的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法:

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
posted @ 2024-01-22 16:54  二次元攻城狮  阅读(57)  评论(0编辑  收藏  举报