[譯]有關Kotlin類型別名(typealias)你須要知道的一切

翻譯說明:html

原標題: All About Type Aliases in Kotlinjava

原文地址: typealias.com/guides/all-…android

原文做者: Dave Leeds程序員

你是否經歷過像下面的對話?數據庫

但願你在現實生活中沒有像這樣的對話,可是這樣情景可能會出如今你的代碼中。安全

例如,看下這個代碼:app

interface RestaurantPatron {
    fun makeReservation(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
    fun visit(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
    fun complainAbout(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
}
複製代碼

當你看到不少類型的代碼被擠在一塊兒的時候,你很容易迷失在代碼的細節中。事實上,僅僅看這些函數的聲明就感受挺嚇人的。ide

幸運的是,Kotlin爲咱們提供了一種簡單的方法來將複雜類型簡化成更具可讀性的別名。函數

在這篇文章中:工具

  • 咱們將學習關於類型別名的一切內容以及他們的工做原理。
  • 而後,咱們將看看你可能會使用到關於它們的一些方法。
  • 而後,咱們將會看下有關它們須要注意的點。
  • 最後,咱們來看看一個相似的概念, Import As, 並看看它們之間的比較。

介紹Type Aliases(類型別名)

一旦咱們爲某個概念創造了一個術語,其實咱們就不必每次談論到它的時候都要去描述一下這個概念,咱們只須要使用這個術語就能夠了! 因此讓咱們代碼也去作相似事情吧。讓咱們來看看這個複雜的類型並給它一個命名。

針對上面的代碼,咱們將經過建立一個類型的別名來優化它:

typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance>
複製代碼

如今,在每一個描述restaurant概念的地方,而不是每次都去寫出 Organization<(Currency, Coupon?) -> Sustenance> 聲明,而是能夠像下面這樣表達出 Restaurant術語:

interface RestaurantPatron {
    fun makeReservation(restaurant: Restaurant)
    fun visit(restaurant: Restaurant)
    fun complainAbout(restaurant: Restaurant)
}
複製代碼

哇! 這樣看上去容易多了,並且當你看到它時,你在代碼中的疑惑也會少不少。

咱們還避免了不少在整個RestaurantPatron接口中大量重複的類型,而不是每次都須要去寫Organization<(Currency, Coupon?) -> Sustenance>,咱們僅僅只有一種類型Restaurant便可。

這樣也就意味着若是咱們須要修改這種複雜類型也是很方便的。例如,若是咱們須要將原來的 Organization<(Currency, Coupon?) -> Sustenance> 化簡成 Organization<(Currency, Coupon?) -> Meal>,咱們僅僅只須要改變一處便可,而不是像原來那樣定義須要修改三個地方。

typealias Restaurant = Organization<(Currency, Coupon?) -> Meal>
複製代碼

簡單!

你或許會思考...

可讀性

你可能會對本身說,「我不明白這是如何有助於代碼的可讀性的...,因爲上述的示例中參數的名稱已經明確代表了restaurant的概念,爲何我還須要一個Restaurant類型呢?難道咱們不能使用具體的參數名稱和抽象類型嗎?」

是的,參數的名稱確實它應該能夠更具體地表示類型,可是咱們上面的RestaurantPatron接口的別名版本仍然更具備可讀性,而且也不容易受到侵入

然而,有些狀況下是沒有命名的,或者說他們沒有一個確切類型名稱,例如Lambda表達式的類型:

interface RestaurantService {
    var locator: (String, ZipCode) -> List<Organization<(Currency, Coupon?) -> Sustenance>>
}
複製代碼

在上面那段代碼中,仍然在表示locator這個lambda表示式正在返回一個restaurant的列表,可是獲取這些表示含義的信息惟一線索就是接口的名稱。然而僅僅從locator函數類型中沒有那麼明確獲得,由於冗長的類型定義已經失去了含義本質。

而下面的這個版本,只須要看一眼就能很容易理解:

interface RestaurantService {
    var locator: (String, ZipCode) -> List<Restaurant>
}
複製代碼

間接性

你或許會想,「等等,我須要更多地考慮類型別名嗎?以前沒有類型別名的時候,把確切真實的類型直接暴露在外部聲明處,如今卻要將他們隱藏在別名的後面」

固然,咱們已經引入了一層間接尋址-有些被別名掩蓋了具體類型細節。可是做爲程序員,咱們一直在作着隱藏命名背後的細節事情。例如:

  • 咱們不會把具體常量數值9.8寫到咱們代碼中,而是咱們建立一個靜態常量ACCELERATION_DUE_TO_GRAVITY,在代碼中直接使用靜態常量。
  • 咱們不會把一個表達式 6.28 * radius 實現寫在代碼任何地方,而是把這個表達式放入到一個 circumference() 函數中去,而後在代碼中去調用circumference() 函數

記住-若是咱們須要去查看別名背後隱藏細節是什麼,僅僅只須要在IDE中使用Command+Click便可。

繼承性

或者你也許在想,"我爲何須要一個類型別名呢?我可使用繼承方式,來繼承這個複雜類型" 以下所示:

class Restaurant : Organization<(Currency, Coupon?) -> Sustenance>()
複製代碼

沒錯,在這種狀況下,你確實能夠經過其詳細的類型參數對 Organization 類進行子類化。事實上,你可能在Java中看到了這一點。

可是類型別名適用性很廣,它也適用於你不能或一般不會去繼承的類型。例如:

  • open 一些的類 例如:String,或者Java中的Optional<T>
  • Kotlin中的單例對象實例( object )。
  • 函數類型,例如: (Currency, Coupon?) -> Sustenance
  • 甚至函數接收者類型,例如: Currency.(Coupon?) -> Sustenance

在文章後面的部分,咱們將更多地比較類型別名方法和繼承方法。

理解類型別名(Type Aliases)

咱們已經瞭解過如何簡單地去聲明一個類型別名。如今讓咱們放大一些,這樣咱們就能夠了解建立時發生的原理!

當處理類型別名的時候,咱們有兩個類型須要去思考:

  • 別名(alias)
  • 底層類型(underlying type)

聽說它自己是一個別名(如UserId),或者包含別名(如List<UserId>)的縮寫類型

當Kotlin編譯器編譯您的代碼時,全部使用到的相應縮寫類型將會擴展成原來的全類型。讓咱們看一個更爲完整例子。

class UniqueIdentifier(val value: Int)

typealias UserId = UniqueIdentifier

val firstUserId: UserId = UserId(0)
複製代碼

當編譯器處理上述代碼時,全部對 UserId 的引用都會擴展到 UniqueIdentifier

換句話說,在擴展期間,編譯器大概作了相似於在代碼中搜索別名(UserId)全部用到的地方,而後將代碼中用到的地方逐字地將其別名替換成全稱類型名(UniqueIdentifier)的工做。

你可能已經注意到我使用了「大部分」和「大概」等字樣。 這是由於,雖然這是咱們理解類型別名的一個好的起點,但有一些狀況下Kotlin不徹底是經過逐字替換原理來實現。 咱們將立刻闡述這些內容! 如今,咱們只需記住這個逐字替換原理一般是有效的。

順便說一下,若是你使用IntelliJ IDEA,你會很高興發現IDE對類型別名有一些很好的支持。例如,您能夠在代碼中看到別名和底層類型:

而且能夠快速查看聲明文檔:

類型別名和類型安全

如今咱們已經瞭解了類型別名的基礎知識,下面咱們來探討另外一個例子。這一個使用多個別名例子:

typealias UserId = UniqueIdentifier
typealias ProductId = UniqueIdentifier

interface Store {
    fun purchase(user: UserId, product: ProductId): Receipt
}
複製代碼

一旦咱們拿到了咱們 Store 的一個實例,咱們能夠進行購買:

val receipt = store.purchase(productId, userId)
複製代碼

此時,你是否注意到什麼了?

咱們意外地把咱們的調用參數順序弄反了! userId應該是第一個參數,而productId應該是第二個參數!

爲何編譯器沒有提示咱們這個問題呢?

若是咱們按照上面的逐字替換原理,咱們能夠模擬編譯器擴展出的代碼:

哇!兩個參數類型都擴展爲相同的底層類型!這意味着能夠將它們混在一塊兒使用,而且編譯器沒法分辨出對應參數。

一個重大的發現: 類型別名不會建立新的類型。他們只是給現有類型取了另外一個名稱而已

固然,這也就是爲何咱們能夠給一個沒有子類繼承的非 open的類添加類型別名。

雖然你可能認爲這老是一件壞事,但實際上有些狀況下它是有幫助的!

咱們來比較兩種不一樣的方式對類型命名:

  • 一、使用 類型別名
  • 二、使用 繼承 去建立一個子類型(如上面的繼承部分所述)。

兩種狀況下的底層類型都是String提供者,它只是一個不帶參數並返回String的函數。

typealias AliasedSupplier = () -> String
interface InheritedSupplier : () -> String
複製代碼

如今,咱們去建立一對函數去接收這些提供者:

fun writeAliased(supplier: AliasedSupplier) = 
        println(supplier.invoke())

fun writeInherited(supplier: InheritedSupplier) = 
        println(supplier.invoke())
複製代碼

最後,咱們準備去調用這些函數:

writeAliased { "Hello" }
writeInherited { "Hello" } // Zounds! A compiler error!(編譯器錯誤)
複製代碼

使用lambda表達式的類型別名方式能夠正常運行,而繼承方式甚至不能編譯!相反,它給了咱們這個錯誤信息:

Required: InheritedSupplier / Found: () -> String

事實上,我發現實際調用writeInherited()的惟一方法,像下面這樣拼湊一個冗長的內容。

writeInherited(object : InheritedSupplier {
    override fun invoke(): String = "Hello"
})
複製代碼

因此在這種狀況下,類型別名方式相比基於繼承的方式上更具備優點。

固然,在某些狀況下,類型安全將對您更爲重要,在這種狀況下,類型別名可能不適合您的需求。

類型別名的例子

如今咱們已經很好地掌握了類型別名,讓咱們來看看一些例子!這裏將爲你提供一些關於類型別名的建議:

// Classes and Interfaces (類和接口)
typealias RegularExpression = String
typealias IntentData = Parcelable

// Nullable types (可空類型)
typealias MaybeString = String?

// Generics with Type Parameters (類型參數泛型)
typealias MultivaluedMap<K, V> = HashMap<K, List<V>>
typealias Lookup<T> = HashMap<T, T>

// Generics with Concrete Type Arguments (混合類型參數泛型)
typealias Users = ArrayList<User>

// Type Projections (類型投影)
typealias Strings = Array<out String>
typealias OutArray<T> = Array<out T>
typealias AnyIterable = Iterable<*>

// Objects (including Companion Objects) (對象,包括伴生對象)
typealias RegexUtil = Regex.Companion

// Function Types (函數類型)
typealias ClickHandler = (View) -> Unit

// Lambda with Receiver (帶接收者的Lambda)
typealias IntentInitializer = Intent.() -> Unit

// Nested Classes and Interfaces (嵌套類和接口)
typealias NotificationBuilder = NotificationCompat.Builder
typealias OnPermissionResult = ActivityCompat.OnRequestPermissionsResultCallback

// Enums (枚舉類)
typealias Direction = kotlin.io.FileWalkDirection
// (but you cannot alias a single enum *entry*)

// Annotation (註解)
typealias Multifile = JvmMultifileClass
複製代碼

你能夠基於類型別名能夠作很酷的操做

正如咱們所看到的同樣,一旦建立了別名就能夠在各類場景中使用它來代替底層類型,好比:

  • 在聲明變量類型、參數類型和返回值類型的時候
  • 在做爲類型參數約束和類型參數的時候
  • 在使用比較類型is或者強轉類型的as的時候
  • 在得到函數引用的時候

除了以上那些之外,它還有一些其餘的用法細節。讓咱們一塊兒來看看:

構造器(Constructors)

若是底層類型有一個構造器,那麼它的類型別名也是如此。你甚至能夠在一個可空類型的別名上調用構造函數!

class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?

// Constructing with the alias: 使用別名來構造對象
val member =  MaybeTeamMember("Miguel")

// The above code does *not* expand verbatim to this (which wouldn't compile):(以上代碼不會是逐字擴展成以下沒法編譯的代碼)
val member = TeamMember?("Miguel")

// Instead, it expands to this:(而是擴展以下代碼)
val member = TeamMember("Miguel")
複製代碼

因此你能夠看到編譯時的擴展並不老是逐字擴展的,在這個例子中就是頗有效的說明。

若是底層類型自己就沒有構造器(例如接口或者類型投影),天然地你也不可能經過別名來調用構造器。

伴生對象

你能夠經過含有伴生對象類的別名來調用該類的伴生對象中的屬性和方法。即便底層類型具備指定的具體類型參數,也是如此。一塊兒來看下:

class Container<T>(var item: T) {
    companion object {
        const val classVersion = 5
    }
}

// Note the concrete type argument of String(注意此處的String是具體的參數類型)
typealias BoxedString = Container<String>

// Getting a property of a companion object via an alias:(經過別名獲取伴侶對象的屬性:)
val version = BoxedString.classVersion

// The line above does *not* expand to this (which wouldn't compile):(這行代碼不會是擴展成以下沒法編譯的代碼)
val version = Container<String>.classVersion

// Instead, it expands to this:(它是會在即將進入編譯期會擴展成以下代碼)
val version = Container.classVersio
複製代碼

咱們再次看到Kotlin並不老是逐字替換擴展的,特別是在其餘狀況下是有幫助的。

須要注意的點

在你使用類型別名的時候,這有一些注意的點你須要記住。

只能定義在頂層位置

類型別名只能定義在代碼頂層位置,換句話說,他們不能被內嵌到一個類、對象、接口或者其餘的代碼塊中。若是你執意要這樣作,你將會獲得一個來自編譯器的錯誤:

Nested and local type aliases are not supported.(不支持嵌套和本地類型別名)

然而,你能夠限制類型別名的訪問權限,好比像常見的訪問權限修飾符internalprivate。因此若是你想要讓一個類型別名只能在一個類中被訪問,你只須要將類型別名和這個類放在同一個文件便可,而且這個別名標記爲private來修飾,好比像這樣:

private typealias Message = String

object Messages {
    val greeting: Message = "Hello"
}
複製代碼

有趣的是,這個private類型別名能夠出如今公共區域,例如以上的代碼 greeting: Message

與Java的互操做性

你能在Java代碼中使用Kotlin的類型別名嗎?

你不能,它們在Java中是不可見的。

可是,若是在Kotlin代碼你有引用類型別名,相似這樣的:

typealias Greeting = String

fun welcomeUser(greeting: Greeting) {
    println("$greeting, user!")
}
複製代碼

雖然你的Java代碼不能使用別名,可是能夠經過使用底層類型繼續與它交互,相似這樣:

// Using type String here instead of the alias Greeting(使用String類型,而不是使用別名Greeting)
String hello = "Hello";
welcomeUser(hello);
複製代碼

遞歸別名

總的來講能夠爲別名取別名:

typealias Greeting = String
typealias Salutation = Greeting 
複製代碼

然而,你明確不能有一個遞歸類型別名定義:

typealias Greeting = Comparable<Greeting>
複製代碼

編譯器會拋出以下異常信息:

Recursive type alias in expansion: Greeting

類型投影

若是你建立了一個類型投影,請注意你指望的樣子。例如,咱們有這樣的代碼:

class Box<T>(var item: T)
typealias Boxes<T> = ArrayList<Box<T>>

fun read(boxes: Boxes<out String>) = boxes.forEach(::println)

複製代碼

而後咱們就指望它這樣定義:

val boxes: Boxes<String> = arrayListOf(Box("Hello"), Box("World"))
read(boxes) // Oops! Compiler error here.(這裏有編譯錯誤)
複製代碼

這個報錯誤的緣由是 Boxes<out String> 會擴展成 ArrayList<Box<out T>> 而不是 ArrayList<out Box<out T>>

Import As: 類型別名(Type Alias)的親兄弟

這裏有個很是相似於類型別名(type alias)的概念,叫作 Import As. 它容許你給一個類型、函數或者屬性一個新的命名,而後你能夠把它導入到一個文件中。例如:

import android.support.v4.app.NotificationCompat.Builder as NotificationBuilder
複製代碼

在這種狀況下,咱們從NotificationCompat導入了Builder類,可是在當前文件中,它將以名稱NotificationBuilder的形式出現。

你是否遇到過須要導入兩個同名的類的狀況?

若是有,那麼你能夠想象一下 Import As將會帶來巨大的幫助,由於它意味着你不須要去限定這些類中某個類。

例如,查看如下Java代碼,咱們能夠將數據庫模型中的User轉換爲service模型的User。

package com.example.app.service;

import com.example.app.model.User;

public class UserService {
    public User translateUser(com.example.app.database.User user) {
        return new User(user.getFirst() + " " + user.getLast());
    }
}
複製代碼

因爲此代碼處理兩個不一樣的類,可是這兩個類都叫User,所以咱們沒法將它們二者都同時導入。相反,咱們只能將其中某個以類名+包名全稱使用User。

利用Kotlin中的 Import As, 咱們就不須要以全稱類名的形式使用,我僅僅只須要給它另外一個命名,而後去導入它便可。

package com.example.app.service

import com.example.app.model.User
import com.example.app.database.User as DatabaseUser

class UserService {
    fun translateUser(user: DatabaseUser): User =
            User("${user.first} ${user.last}")
}
複製代碼

此時的你,或許想知道,類型別名(type alias)和 Import As之間的區別?畢竟,您還能夠用typealias消除User引用的衝突,以下所示:

package com.example.app.service

import com.example.app.model.User

typealias DatabaseUser = com.example.app.database.User

class UserService {
    fun translateUser(user: DatabaseUser): User =
            User("${user.first} ${user.last}")
}
複製代碼

沒錯,事實上,除了元數據(metadata)以外,這兩個版本的UserService均可以編譯成相同的字節碼!

因此,問題來了,你怎麼去選擇你須要那一個?它們之間有什麼不一樣? 這裏列舉了一系列有關 typealiasimport as 各自支持特性狀況以下:

目標對象(Target) 類型別名(Type Alias) Import As
Interfaces and Classes yes yes
Nullable Types yes no
Generics with Type Params yes no
Generics with Type Arguments yes no
Function Types yes no
Enum yes yes
Enum Members no yes
object yes yes
object Functions no yes
object Properties no yes

正如你所看到的,一些目標對象僅僅被支持一種或多種。

這兒有一些內容須要被牢記:

  • 類型別名能夠具備可見性修飾符,如internal和private,而它訪問的範圍是整個文件。
  • 若是您從已經自動導入的包中導入類,例如kotlin.*或kotlin.collections*,那麼您必須經過該名稱引用它。 例如,若是您要將import kotlin.String寫爲RegularExpression,則String的用法將引用java.lang.String.

順便說一下,若是您是Android開發人員,而且在您的項目中使用到了 Kotlin Android Extensions,那麼使用import as將是一個美妙的方式去重命名來自於Activity中對應佈局的id,將原來佈局中下劃線分割的id,能夠重命名成駝峯形式,使你的代碼更具備可讀性。例如:

import kotlinx.android.synthetic.main.activity.upgrade_button as upgradeButton
複製代碼

這可使您從findViewById()(或Butter Knife)轉換到Kotlin Android Extensions變得很是簡單!

總結

使用類型別名是一種很好的方式,它能夠爲複雜,冗長和抽象的類型提供簡單,簡潔和特定於域的名稱。它們易於使用,而且IDE工具支持可以讓您深刻了解底層類型。在正確的地方使用,它們可使您的代碼更易於閱讀和理解。

譯者有話說

  • 一、爲何我要翻譯這篇博客?

typealias類型別名,可能有的Kotlin開發人員接觸到過,有的尚未碰到過。接觸過的,可能也用得很少,不知道如何更好地使用它。這篇博客很是好,能夠說得上是Kotlin中的typealias的深刻淺出。它闡述了什麼是類型別名、類型別名的使用場景、類型別名的實質原理、類型別名和import as對比以及類型別名中須要注意的坑。看完這篇博客,彷彿打開kotlin中的又一個新世界,你將會很神奇發現一個小小typealias卻如此強大,深刻實質原理你又會發現原來也挺簡單的,可是無不被kotlin這門語言設計思想所折服,使用它能夠大大簡化代碼以及提高代碼的可讀性。因此對於Kotlin的初學者以及正在使用kotlin開發的你來講,它可能會對你有幫助。

  • 二、這篇博客中幾個關鍵點和注意點。

關於typealias我以前有篇博客淺談Kotlin語法篇之Lambda表達式徹底解析(六)也大概介紹了下,可是這篇博客已經介紹的很是詳細,這裏再次強調其中比較重要幾點:

  • 類型別名(typealias)不會建立新的類型。他們只是給現有類型取了另外一個名稱而已.
  • typealias實質原理,大部分狀況下是在編譯時期採用了逐字替換的擴展方式,還原成真正的底層類型;可是不是徹底是這樣的,正如本文例子提到的那樣。
  • typealias只能定義在頂層位置,不能被內嵌在類、接口、函數等內部
  • 使用import as對於已經使用Kotlin Android Extension 或者anko庫的Android開發人員來講很是棒。看下如下代碼例子:

沒有使用import as

//使用anko庫直接引用佈局中下劃線id命名,看起來挺彆扭,不符合駝峯規範。
import kotlinx.android.synthetic.main.review_detail.view.*

class WidgetReviewDetail(context: Context, parent: ViewGroup){

     override fun onViewCreated() {
		mViewRoot.run {
			review_detail_tv_checkin_days.isBold()
			review_detail_tv_course_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			review_detail_tv_elevator_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			review_detail_tv_word_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
		}
	}

	override fun renderWidget(viewModel: VModelReviewDetail) = with(viewModel) {
			mViewRoot.review_detail_iv_avatar.loadUrl(url = avatarUrl)
			mViewRoot.review_detail_tv_checkin_days.text = labelCheckInDays
			mViewRoot.review_detail_tv_word_num.text = labelWordNum
			mViewRoot.review_detail_tv_elevator_num.text = labelElevatorNum
			mViewRoot.review_detail_tv_course_num.text = labelCourseNum
	}
}	
複製代碼

使用import as 總體代碼更加簡單和更具備可讀性,此外還有一個好處就是佈局文件ID變了,只須要import as聲明處修改便可,無需像以前那樣每一個用到的地方都須要修改

注意的一點是若是給每一個View組件都用import as感受又回到從新回到findViewById的,又會產生冗長聲明,這裏建議你慎重使用。可是此處出發點不同,目的在於簡化冗長的id命名的使用。

import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_checkin_days as tvCheckInDays
import kotlinx.android.synthetic.main.review_detail.view.review_detail_iv_avatar as ivAvatar
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_word_num as tvWordNum
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_elevator_num as tvElevatorNum
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_course_num as tvCourseNum

class WidgetReviewDetail(context: Context, parent: ViewGroup){

        override fun onViewCreated() {
		mViewRoot.run {
			tvCheckInDays.isBold()
			tvCourseNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			tvElevatorNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			tvWordNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
		}
	}

	override fun renderWidget(viewModel: VModelReviewDetail) {
		with(viewModel) {
			mViewRoot.ivAvatar.loadUrl(url = avatarUrl)
			mViewRoot.tvCheckInDays.text = labelCheckInDays
			mViewRoot.tvWordNum.text = labelWordNum
			mViewRoot.tvElevatorNum.text = labelElevatorNum
			mViewRoot.tvCourseNum.text = labelCourseNum
		}
	}
}
複製代碼

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索