面向協(xié)議編程

此文為資料匯總文,基于自己的理解收集網(wǎng)絡上簡明易懂的文章及例子,通篇瀏覽之后會對這個概念會有初步的認識。

參考資料:面向“接口”編程和面向“實現(xiàn)”編程

Protocol-Oriented Programming in Swift

接口編程那些事

Introducing Protocol-Oriented Programming in Swift 2

IF YOU'RE SUBCLASSING, YOU'RE DOING IT WRONG.

and so on...

因為簡書的Markdown 不支持 [toc]生成目錄,另外一種方式生成目錄較為繁瑣,所以貼個圖片版的目錄,另外希望簡書早點支持[toc]:

目錄

什么叫做面向協(xié)議編程

我自己感覺比較明確的定義就是Apple wwdc15/408視頻中的那句話:

Don't start with a class.
Start with a protocol.

從協(xié)議的角度開始編程??

談談面對對象編程(OOP)的一些弊端

姿勢不錯

如圖如何走心。

  • 面對對象的目的是大規(guī)模重用代碼。

  • 面對對象的手段是綁定結(jié)構(gòu)和函數(shù)。

  • 面對對對象的哲學含義是形象化抽象一個虛擬物體。

以上三個點可謂是面對對象編程的定義以及面對對象的好處,一旦聊到面對對象總會伴隨 “代碼重用”。我們從真實的世界來考慮這個問題,我們對客觀存在的主體看法是會隨著時間的改變而改變的,真實世界中甚至不存在固定形式化的抽象,而代碼是為了具體問題而出現(xiàn)的,所以不存在通用抽象,也就不存在可以無限重用的定義和邏輯。所以對象也就是用于計算的模型而已,技術手段是正確的(給數(shù)據(jù)綁定操作) 但是對于目標(大規(guī)模代碼重用)相去甚遠,能重用的應該只有為了解決問題的方法,而不是只有模型。另外的難點,不同人為了解決相似問題,開發(fā)出來的模型可以十萬八千里,為了重用模型,修改之后又能適應新問題,于是這叫泛化,它估計你去制造全能模型,但是不僅難,還沒法向后兼容,有時候就硬是要把飛機做成魚……這就是面向?qū)ο笏季S的硬傷,創(chuàng)造出一個大家都懂,大家都認為對,大家都能拿去用的模型太難?。ㄕ灾蹙x)

我自己的感覺,類的繼承讓代碼的可讀性大大降低,比如我想知道這個類用途還要去看這個類的父類能干嘛假如它還有個祖父類呢?而且想想看假如一個項目由一個基類開始,并伴生了很多子類,解決需求發(fā)現(xiàn)需要更改基類的時候,不敢動手是多么恐怖的一件事情。

Java程序員對單個方法的實現(xiàn)超過10行感到非常不安,這代表自己的代碼可重用性很差。于是他把一個3個參數(shù)的長方法拆成了4個子過程,每個子過程有10個以上的參數(shù)。后來他覺得這樣很不OOP,于是他又創(chuàng)建了4個interface和4個class。

由一個簡單的例子開始

讓我們由這個例子開始面向“協(xié)議”編程

例子采用Rust語言,編輯器推薦使用CodeRunner

先用面對對象的視角,書可以燃燒,于是書有個方法 burn()。
書并不是唯一會燃燒的東西,木頭也可以燃燒,它也有一個方法叫做 burn()。看看不是面向“協(xié)議”下是如何燃燒:

struct Book {
    title: @str,
    author: @str,
}

struct Log {
    wood_type: @str,
}

這兩個結(jié)構(gòu)體分別表示書(Book)和木頭(Log),下面實現(xiàn)它們的方法:

impl Log {
    fn burn(&self) {
        println(fmt!("The %s log is burning!", self.wood_type));
    }
}

impl Book {
    fn burn(&self) {
        println(fmt!("The book %s by %s is burning!", self.title, self.author));
    }
}

現(xiàn)在書與木頭都有了 burn() 方法,現(xiàn)在我們燒它們。

先放木頭:

fn start_fire(lg: Log) {
    lg.burn();
}

fn main() {
    let lg = Log {
        wood_type: @"Oak",
        length: 1,
    };

    // Burn the oak log!
    start_fire(lg);
}

一切ok,輸出 "The Oak log is burning!"。

現(xiàn)在因為我們已經(jīng)有了一個 start_fire 函數(shù),是否我們可以把書也傳進去,因為它們都有 burn()

fn main() {
    let book = Book {
        title: @"The Brothers Karamazov",
        author: @"Fyodor Dostoevsky",
    };

    // Let's try to burn the book...
    start_fire(book);
}

可行么?肯定不行?。『瘮?shù)已經(jīng)指名需要Log結(jié)構(gòu)體,而不是Book結(jié)構(gòu)體,怎么解決這個問題,再寫一個函數(shù)接受Book結(jié)構(gòu)體?這樣只會得到兩個幾乎一樣的函數(shù)。

解決這個問題

加一個協(xié)議接口,協(xié)議接口在Rust語言中叫做 trait

struct Book {
    title: @str,
    author: @str,
}

struct Log {
    wood_type: @str,
}

trait Burnable {
    fn burn(&self);
}

多了一個 Burnable 的接口,為每個結(jié)構(gòu)體實現(xiàn)它們的接口:

impl Burnable for Log {
    fn burn(&self) {
        println(fmt!("The %s log is burning!", self.wood_type));
    }
}

impl Burnable for Book {
    fn burn(&self) {
        println(fmt!("The book \"%s\" by %s is burning!", self.title, self.author));
    }
}

接下來實現(xiàn)點火函數(shù)

fn start_fire<T: Burnable>(item: T) {
    item.burn();
}

這里Swift跟Rust很像,T 占位符表示任何實現(xiàn)了這個接口的類型。

這樣我們只要往函數(shù)里面?zhèn)魅我鈱崿F(xiàn)了 Burnable 協(xié)議接口的類型就沒有問題。主函數(shù):

fn main() {
    let lg = Log {
        wood_type: @"Oak",
    };

    let book = Book {
        title: @"The Brothers Karamazov",
        author: @"Fyodor Dostoevsky",
    };

    // Burn the oak log!
    start_fire(lg);

    // Burn the book!
    start_fire(book);
}

成功輸出:

The Oak log is burning!

The book “The Brothers Karamazov” by Fyodor Dostoevsky is burning!

于是這個函數(shù)完全能復用任意實現(xiàn)了 Burnable 協(xié)議接口的實例,cool...

在Objective-C中如何面對協(xié)議編程

OC畢竟是以面向?qū)ο鬄樵O計基礎的,所以實現(xiàn)比較麻煩,接口在OC中為Protocol,Swift中強化了Protocol協(xié)議的地位(下節(jié)再講Swift中的面向協(xié)議)。

目前大部分開發(fā)以面向?qū)ο缶幊虨橹鳎热缡褂?ASIHttpRequest 來執(zhí)行網(wǎng)絡請求:

ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDidFinishSelector:@selector(requestDone:)];
[request setDidFailSelector:@selector(requestWrong:)];
[request startAsynchronous];

發(fā)起請求的時候,我們需要知道要給request對象賦值哪一些屬性并調(diào)用哪一些方法,現(xiàn)在來看看 AFNetworking 的請求方式:

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"www.olinone.com" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"good job");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    //to do
}];

一目了然,調(diào)用者不用關心它有哪些屬性,除非接口無法滿足需求需要去了解相關屬性的定義。這是兩種完全不同的設計思路。

接口比屬性直觀

定義一個對象的時候,一般都要為它定義一些屬性,比如 ReactiveCocoa 中的 RACSubscriber 對象定義:

@interface RACSubscriber ()
  
@property (nonatomic, copy) void (^next)(id value);
@property (nonatomic, copy) void (^error)(NSError *error);
@property (nonatomic, copy) void (^completed)(void);
  
@end

以接口的形式提供入口:

@interface RACSubscriber
  
+ (instancetype)subscriberWithNext:(void (^)(id x))next
                             error:(void (^)(NSError *error))error
                         completed:(void (^)(void))completed;
  
@end

接口比屬性更加直觀,抽象的接口直接描述要做的事情。

接口依賴

設計一個APIService對象

@interface ApiService : NSObject
  
@property (nonatomic, strong) NSURL        *url;
@property (nonatomic, strong) NSDictionary *param;
  
- (void)execNetRequest;
  
@end

正常發(fā)起Service請求時,調(diào)用者需要直接依賴該對象,起不到解耦的目的。當業(yè)務變動需要重構(gòu)該對象時,所有引用該對象的地方都需要改動。如何做到既能滿足業(yè)務又能兼容變化?抽象接口也許是一個不錯的選擇,以接口依賴的方式取代對象依賴,改造代碼如下:

@protocol ApiServiceProtocol  
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param;
  
@end
  
@interface NSObject (ApiServiceProtocol) <ApiServiceProtocol>  
@end
  
@implementation NSObject (ApiServiceProtocol)
  
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    ApiService *apiSrevice = [ApiService new];
    apiSrevice.url = url;
    apiSrevice.param = param;
    [apiSrevice execNetRequest];
}
  
@end

通過接口的定義,調(diào)用者可以不再關心ApiService對象,也無需了解其有哪些屬性。即使需要重構(gòu)替換新的對象,調(diào)用邏輯也不受任何影響。調(diào)用接口往往比訪問對象屬性更加穩(wěn)定可靠。

抽象對象

定義ApiServiceProtocol可以隱藏ApiService對象,但是受限于ApiService對象的存在,業(yè)務需求發(fā)生變化時,仍然需要修改ApiService邏輯代碼。如何實現(xiàn)在不修改已有ApiService業(yè)務代碼的條件下滿足新的業(yè)務需求?

參考Swift抽象協(xié)議的設計理念,可以使用Protocol抽象對象,畢竟調(diào)用者也不關心具體實現(xiàn)類。Protocol可以定義方法,可是屬性的問題怎么解決?此時,裝飾器模式也許正好可以解決該問題,讓我們試著繼續(xù)改造ApiService

@protocol ApiService <ApiServiceProtocol>
 
// private functions
 
@end
 
@interface ApiServicePassthrough : NSObject
 
@property (nonatomic, strong) NSURL        *url;
@property (nonatomic, strong) NSDictionary *param;
 
- (instancetype)initWithApiService:(id<ApiService>)apiService;
- (void)execNetRequest;
 
@end
@interface ApiServicePassthrough ()
 
@property (nonatomic, strong) id<ApiService> apiService;
 
@end
 
@implementation ApiServicePassthrough
 
- (instancetype)initWithApiService:(id<ApiService>)apiService {
    if (self = [super init]) {
        self.apiService = apiService;
    }
    return self;
}
 
- (void)execNetRequest {
    [self.apiService requestNetWithUrl:self.url Param:self.param];
}
 
@end

經(jīng)過Protocol的改造,ApiService對象化身為ApiService接口,其不再依賴于任何對象,做到了真正的接口依賴取代對象依賴,具有更強的業(yè)務兼容性

定義一個Get請求對象

@interface GetApiService : NSObject  <ApiService>
 
@end
 
@implementation GetApiService
 
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    // to do
}
 
@end

改變請求代碼

@implementation NSObject (ApiServiceProtocol)
 
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    id<ApiService> apiSrevice = [GetApiService new];
    ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
    apiServicePassthrough.url = url;
    apiServicePassthrough.param = param;
    [apiServicePassthrough execNetRequest];
}
 
@end

對象可以繼承對象,Protocol也可以繼承Protocol,并且可以繼承多個Protocol,Protocol具有更強的靈活性。某一天,業(yè)務需求變更需要用到新的Post請求時,可以不用修改 GetApiService一行代碼,定義一個新的 PostApiService實現(xiàn)Post請求即可,避免了對象里面出現(xiàn)過多的if-else代碼,也保證了代碼的整潔性。

依賴注入

GetApiService依然是以對象依賴的形式存在,如何解決這個問題?沒錯,依賴注入!依賴注入會讓測試變得可行。

關于依賴注入可以看這篇文章

借助 objection 開源庫改造ApiService:

@implementation NSObject (ApiServiceProtocol)
 
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    id<ApiService> apiSrevice = [[JSObjection createInjector] getObject:[GetApiService class]];
    ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
    apiServicePassthrough.url = url;
    apiServicePassthrough.param = param;
    [apiServicePassthrough execNetRequest];
}
 
@end

調(diào)用者關心請求接口,實現(xiàn)者關心需要實現(xiàn)的接口,各司其職,互不干涉。

我自己的感覺,利用OC來進行面向協(xié)議編程還是繞不過這個坎,反而變成為了面向協(xié)議編程而進行協(xié)議編程,比較捉雞。

Swift中的面向協(xié)議編程

WWDC15上表示
Swift is a Protocol-Oriented Programming Language

Talk is cheap,show you the code!

先看看這個例子

class Ordered {
  func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}

class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < (other as! Number).value
  }
}

as!在swift中表示強制類型轉(zhuǎn)換。
對于這種情況,蘋果的工程師表示這是一種 Lost Type Relationships,我對這個的理解 是 失去對類型的控制。也就是這個Number類的函數(shù)往里邊傳非Number類型的參數(shù)會出問題??赡苣銈冇X得這個問題還好,只要注意下Number下函數(shù)的函數(shù)實現(xiàn)就好了,但是在大型項目中,你使用一個類因為擔心類型問題而需要去看類的實現(xiàn),這樣的編碼是不是很讓人煩躁?

利用Protocol來重寫

直接上代碼吧:

protocol Ordered {
  func precedes(other: Self) -> Bool
}
struct Number : Ordered {
  var value: Double = 0
  func precedes(other: Number) -> Bool {
    return self.value < other.value
  }
}

用swift中的struct(結(jié)構(gòu)體)來取代class
protocol 的Self表示任何遵循了這個協(xié)議的類型?,F(xiàn)在就不用擔心類型的問題了。

struct與class的區(qū)別

struct是值拷貝類型,而class是引用類型。這也是apple的工程師推薦使用struct代替class的原因。

struct無法繼承,class可以繼承。

關于值拷貝與引用的區(qū)別看下面的
code:

struct Dog{
    var owner : String?
}

var 梅西的狗 = Dog(owner:"梅西")
var C羅的狗 = Dog(owner:"C羅")
var 貝爾的狗 = Dog(owner:"貝爾")

print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 梅西與C羅

C羅的狗 = 梅西的狗

print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 梅西與梅西

梅西的狗 = 貝爾的狗

print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 貝爾與梅西 
//C羅的狗.owner還是梅西

//使用class
class DogClass{
    var owner : String?
}

var 梅西的狗 = DogClass()
梅西的狗.owner = "梅西"

var C羅的狗 = DogClass()
C羅的狗.owner = "C羅"

var 貝爾的狗 = DogClass()
貝爾.owner = "貝爾"

print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 梅西與C羅

C羅的狗 = 梅西的狗
print(C羅的狗.owner)
//此行輸出 梅西

梅西的狗.owner = 貝爾的狗.owner

print(梅西的狗.owner,"與",C羅的狗)
//此行輸出 貝爾與貝爾 
// C羅的狗的owner也變?yōu)樨悹柫?

再插入一幅圖來理解引用類型吧:

簡單的運用下我們定義的這個協(xié)議吧

以下是一個簡單的二分查找算法函數(shù)實現(xiàn):

func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int {
  var lo = 0
  var hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }
    else { hi = mid }
}
return lo }

其中T(可以理解為 “占位符”)表示任何遵循 Ordered 協(xié)議的類型,這里就和開頭使用Rust語言實現(xiàn)的程序異曲同工了。

Swift2.0引入的一個重要特性 protocol extension

也就是我們可以擴展協(xié)議,cool。

我們可以定義一個協(xié)議:

protocol MyProtocol {
    func method()
}

然后在這個協(xié)議的extension中增加函數(shù) method() 的實現(xiàn):

extension MyProtocol {
    func method() {
        print("Called")
    }
}

創(chuàng)建一個 struct 遵循這個協(xié)議:

struct MyStruct: MyProtocol {

}

MyStruct().method()
// 輸出:
// Called

這樣就可以實現(xiàn)類似繼承的功能,而不需要成為某個類的子類。
cool嗎?現(xiàn)在我們回過頭來想想,使用OC編程中,系統(tǒng)固有的協(xié)議不借助黑魔法我們是否可以對已有的協(xié)議進行擴展?不能?。P于在OC中如何擴展協(xié)議自行搜索,此處不展開了)。

一個簡單的例子運用 protocol extension

定義一個 Animal 協(xié)議和動物的屬性:

protocol Animal {
    var name: String { get }
    var canFly: Bool { get }
    var canSwim: Bool { get }
}

定義三個具體的動物:

struct Parrot: Animal {
    let name: String
    let canFly = true
    let canSwim = false
}

struct Penguin: Animal {
    let name: String
    let canFly = true
    let canSwim = true
}

struct Goldfish: Animal {
    let name: String
    let canFly = false
    let canSwim = true
}

每一個動物都要實現(xiàn)一遍它們的 canFly 與 canSwim 屬性顯得很業(yè)余。

現(xiàn)在來定義Flyable、Swimable兩個Protocol:

protocol Flyable {
    
}

protocol Swimable {
    
}

利用 extension給protocol添加默認實現(xiàn):

extension Animal {
    var canFly: Bool { return false }
    var canSwim: Bool { return false }
}

extension Animal where Self: Flyable {
    var canFly: Bool { return true }
}

extension Animal where Self: Swimable {
    var canSwim: Bool { return true }
}

這樣符合Flyable協(xié)議的Animal,canFly屬性為true,復合Swimable的Animal,canSwim屬性為true。

改造上面三個結(jié)構(gòu)體:

struct Parrot: Animal, Flyable {
    let name: String
}

struct Penguin: Animal, Flyable, Swimable {
    let name: String
}

struct Goldfish: Animal, Swimable {
    let name: String
}

在將來,你需要改動代碼,比如 Parrot 老了,沒辦法飛了,就將Flyable的協(xié)議去掉即可。

好處:

  • class只能繼承一個class,類型可以遵循多個protocol,就可以同時被多個protocol實現(xiàn)多個默認行為。
  • class,struct,enum都可以遵循protocol,而class的繼承只能是class,protocol能給值類型提供默認的行為。
  • 高度解耦不會給類型引進額外的狀態(tài)。

一個簡單的實戰(zhàn)

這樣一個簡單的需求,一個登陸頁面,用戶輸入的密碼錯誤之后,密碼框會有一個抖動,實現(xiàn)起來很簡單:

import UIKit

class FoodImageView: UIImageView {
    func shake() {
        let animation = CABasicAnimation(keyPath: "position")
        animation.duration = 0.05
        animation.repeatCount = 5
        animation.autoreverses = true
        animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
        animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
        layer.addAnimation(animation, forKey: "position")
    }
}

好了,現(xiàn)在產(chǎn)品告訴你,除了密碼框要抖動,登陸按鈕也要抖動,那這樣:

import UIKit

class ActionButton: UIButton {

    func shake() {
        let animation = CABasicAnimation(keyPath: "position")
        animation.duration = 0.05
        animation.repeatCount = 5
        animation.autoreverses = true
        animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
        animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
        layer.addAnimation(animation, forKey: "position")
    }
}

這已然是兩個重復的代碼,而且當你需要變動動畫時候,你需要改動兩處的代碼,這很不ok,有OC編程經(jīng)驗的人會想到利用Category的方式,在swift中即為extension,改造如下:

import UIKit

extension UIView {

    func shake() {
        let animation = CABasicAnimation(keyPath: "position")
        animation.duration = 0.05
        animation.repeatCount = 5
        animation.autoreverses = true
        animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
        animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
        layer.addAnimation(animation, forKey: "position")
    }
}

這樣看起來似乎已經(jīng)很棒了,因為我們節(jié)約了代碼,但是想一想,有必要為了一部分的視圖增加一個共同的實現(xiàn)而是全部的UIView的類都具有這個 shake() 方法么,而且可讀性很差,特別是當你的UIView的extension中的代碼不停的往下增加變得很冗長的時候:

class FoodImageView: UIImageView {
    // other customization here
}

class ActionButton: UIButton {
    // other customization here
}

class ViewController: UIViewController {
    @IBOutlet weak var foodImageView: FoodImageView!
    @IBOutlet weak var actionButton: ActionButton!

    @IBAction func onShakeButtonTap(sender: AnyObject) {
        foodImageView.shake()
        actionButton.shake()
    }
}

單獨看 FoodImageView 類和 ActionButton 類的時候,你看不出來它們可以抖動,而且 share() 函數(shù)到處都可以分布。

利用protocol改造

創(chuàng)建 Shakeable 協(xié)議

//  Shakeable.swift

import UIKit

protocol Shakeable { }

extension Shakeable where Self: UIView {

    func shake() {
        // implementation code
    }
}

借助protocol extension 我們把 shake() 限定在UIView類中,并且只有遵循 Shakeable 協(xié)議的UIView類才會擁有這個函數(shù)的默認實現(xiàn)。

class FoodImageView: UIImageView, Shakeable {

}

class ActionButton: UIButton, Shakeable {

}

可讀性是不是增強了很多?通過這個類的定義來知道這個類的用途這樣的感覺是不是很棒?假如產(chǎn)品看到別家的產(chǎn)品輸入密碼錯誤之后有個變暗的動畫,然后讓你加上,這個時候你只需要定義另外一個協(xié)議 比如 Dimmable 協(xié)議:

class FoodImageView: UIImageView, Shakeable, Dimmable {

}

這樣很方便我們重構(gòu)代碼,怎么說呢,當這個視圖不需要抖動的時候,刪掉 shakeable協(xié)議:

class FoodImageView: UIImageView, Dimmable {

}

嘗試從協(xié)議開始編程吧!

什么時候使用class?

  • 實例的拷貝和比較意義不大的情況下
  • 實例的生命周期和外界因素綁定在一起的時候
  • 實例處于一種外界流式處理狀態(tài)中,形象的說,實例像污水一樣處于一個處理污水管道中。
final class StringRenderer : Renderer {
  var result: String
  ...
}

在Swift中final關鍵字可以使這個class拒絕被繼承。

別和框架作對

  • 當一個框架希望你使用子類或者傳遞一個對象的時候,別反抗。

小心細致一些

  • 編程中不應該存在越來越臃腫的模塊。
  • 當從class中重構(gòu)某些東西的時候,考慮非class的處理方式。

總結(jié)

wwdc視頻中明確表示:

Protocols > Superclasses

Protocol extensions = magic (almost)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容