我們進行了多年的Android開發,但是面對越來越複雜的業務邏輯和越來越龐大的代碼,傳統命令式的編程方式已經漸漸無法解決我們的問題了。今天開始我們將探索一種非常強大的編程範式:函數式編程。
1. 傳統編程範式的挑戰
1.1 過程式編程的難題
大家日常開發中一定遇到過這些問題:
1.1.1 返回值不確定
// 全局計數器變量
var counter = 0
// 返回值依賴於外部狀態,每次調用結果不同
fun getNextId(): Int {
return counter++
}
在Android開發中,這種情況經常出現在多個Fragment或Activity共享狀態時。
1.1.2 屬性值不確定(變量)
// 用户信息存儲在全局變量中
var currentUser: User? = null
// 在不同生命週期階段訪問可能得到不同結果
fun displayUserInfo() {
// currentUser可能在任何時候被其他組件修改
currentUser?.let {
binding.userName.text = it.name
}
}
1.1.3 回調地獄
這個在Android開發中實在太常見了:
// 經典的Android回調地獄
userRepository.getUser { user ->
postRepository.getPostsByUser(user.id) { posts ->
imageLoader.loadProfileImage(user.avatarUrl) { avatar ->
analyticsService.trackUserView(user.id) {
// 此時邏輯已經嵌套四層
updateUI(user, posts, avatar)
}
}
}
}
1.1.4 線程不安全
// 多線程環境下共享變量的問題
class SharedCounter {
var count = 0
fun increment() {
// 在多線程環境下,這個操作不是原子的
count++
}
}
1.2 面向對象編程的難題
1.2.1 類的單繼承
Android開發中非常常見的問題:
// Android中的經典問題
abstract class BaseActivity : AppCompatActivity() {
// 基礎功能
}
// 如果我們想同時繼承另一個基類,就無法做到
class MyActivity : BaseActivity(), SomeInterface {
// 無法再繼承其他基類
}
1.2.2 組合性差
// 為了適應不同場景,方法重載成災
class UserManager {
fun fetchUser(id: Int) { /* ... */ }
fun fetchUser(email: String) { /* ... */ }
fun fetchUser(id: Int, withPosts: Boolean) { /* ... */ }
fun fetchUser(id: Int, withPosts: Boolean, withComments: Boolean) { /* ... */ }
// 隨着需求增加,重載方法數量呈指數級增長
}
1.2.3 模板代碼眾多
// 標準的Android RecyclerView Adapter模板代碼
class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
private val users = mutableListOf<User>()
fun updateUsers(newUsers: List<User>) {
users.clear()
users.addAll(newUsers)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// 樣板代碼...
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// 樣板代碼...
}
override fun getItemCount() = users.size
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
// 樣板代碼...
}
}
2. 函數式編程的解決方案
2.1 不可變性(Immutability)
2.1.1 變量的問題(值的不確定性)
如果經歷的項目足夠多,就能發現,"變量"往往是很多Bug的根本來源。普通的變量改變的時間不確定、變更的值不確定,使得防禦性檢查在某些情況下也會失效。
來看一個Android中的實際例子:
// 傳統方式:使用可變列表
class PostsViewModel : ViewModel() {
val posts = mutableListOf<Post>()
fun loadPosts() {
// 在某個線程中加載數據
repository.getPosts { newPosts ->
posts.clear()
posts.addAll(newPosts)
// 此時如果有其他線程正在讀取posts...
}
}
}
2.1.2 使用val代替var
Kotlin中的val關鍵字幫助我們定義不可變引用:
// 函數式方式:使用不可變引用
class PostsViewModel : ViewModel() {
// 使用LiveData或Flow來表示變化的狀態
private val _posts = MutableLiveData<List<Post>>(emptyList())
val posts: LiveData<List<Post>> = _posts
fun loadPosts() {
repository.getPosts { newPosts ->
// 整體替換,不是修改
_posts.value = newPosts
}
}
}
這裏的關鍵區別是:我們不是修改列表本身,而是創建一個新的列表替換掉舊的。
2.1.3 data class(對象值變化的判定)
Kotlin的data class為不可變編程提供了極好的支持:
// 不可變的數據模型
data class User(
val id: Int,
val name: String,
val email: String
)
// 需要修改時,創建新對象而非修改原對象
val user = User(1, "John", "john@example.com")
val updatedUser = user.copy(name = "John Doe")
2.1.4 不可變容器
RecyclerView中的一個常見問題:
// 傳統方式:使用可變列表容易出現異步問題
class PostAdapter : RecyclerView.Adapter<PostViewHolder>() {
private val posts = mutableListOf<Post>()
fun updatePosts(newPosts: List<Post>) {
posts.clear()
posts.addAll(newPosts)
notifyDataSetChanged()
}
override fun getItemCount() = posts.size
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
// 如果在getItemCount()後但在這個方法前,
// 其他線程修改了posts,可能導致IndexOutOfBoundsException
val post = posts[position]
holder.bind(post)
}
}
函數式解決方案:
// 函數式方式:使用不可變列表
class PostAdapter : RecyclerView.Adapter<PostViewHolder>() {
// 使用不可變列表
private var posts: List<Post> = emptyList()
fun updatePosts(newPosts: List<Post>) {
// DiffUtil計算差異,高效更新
val diffResult = DiffUtil.calculateDiff(PostDiffCallback(posts, newPosts))
// 整體替換而非修改
posts = newPosts
diffResult.dispatchUpdatesTo(this)
}
override fun getItemCount() = posts.size
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
// posts是一個不變的引用,安全地訪問
val post = posts[position]
holder.bind(post)
}
}
2.1.5 性能問題?
對於一些開發者而言,"每次創建新對象"會引起性能擔憂。但事實上:
- 結構共享:不可變容器在修改後創建的新對象並不會將舊容器中所有數據全部深度拷貝。Kotlin標準庫中的
persistent collections就採用了這種優化。 - 優化空間:由於"不可變"這個基本共識,反而可以做出很多優化。
- VM優化:現代JVM對於短生命週期的小對象有專門的優化(年輕代GC)。
- 權衡取捨:在Android應用開發中,我們更多面對的是善變的需求和複雜的業務邏輯,而不是極端性能場景。不可變性帶來的代碼健壯性遠超過其微小的性能損失。
來看個實際的例子 - 使用不可變列表的結構共享:
// Haskell中的鏈表實現方式在概念上類似這樣
sealed class ImmutableList<out T> {
object Nil : ImmutableList<Nothing>()
data class Cons<T>(val head: T, val tail: ImmutableList<T>) : ImmutableList<T>()
}
// 添加新元素只需創建一個新的頭節點,複用原有的尾部
fun <T> ImmutableList<T>.prepend(item: T): ImmutableList<T> =
ImmutableList.Cons(item, this)
在Haskell中,這種鏈表實現允許高效地共享結構:
-- 定義一個列表
originalList = [3,4,5]
-- 添加一個元素,但原始列表不變
newList = 2 : originalList -- newList現在是[2,3,4,5]
-- originalList仍然是[3,4,5]
-- 兩個列表共享了[3,4,5]部分的內存
2.2 副作用與純函數
2.2.1 什麼是純函數
純函數是函數式編程的核心概念:對於相同的輸入,總是產生相同的輸出,並且沒有副作用。
// 不純的函數 - 依賴外部狀態
var discount = 0.0
fun calculatePrice(basePrice: Double): Double {
return basePrice * (1 - discount) // 依賴外部變量
}
// 純函數 - 只依賴輸入參數
fun calculatePrice(basePrice: Double, discount: Double): Double {
return basePrice * (1 - discount) // 只依賴參數
}
在Android開發中的一個實際例子:
// 不純的方式
class PriceCalculator {
// 依賴於外部狀態
var taxRate = 0.1
fun calculateTotalPrice(price: Double): Double {
// 使用外部狀態,結果取決於taxRate的當前值
return price * (1 + taxRate)
}
}
// 純函數方式
class PriceCalculator {
// 將所有依賴作為參數傳入
fun calculateTotalPrice(price: Double, taxRate: Double): Double {
// 只依賴於輸入參數,結果可預測
return price * (1 + taxRate)
}
}
2.2.2 無副作用
副作用是指函數在返回值之外對程序狀態的任何修改。包括:
- 修改全局變量或對象
- 寫入文件或數據庫
- 網絡請求
- 顯示在屏幕上
- 拋出異常
Android中的副作用例子:
// 含有副作用的函數
fun saveUser(user: User): Boolean {
try {
database.insert(user) // 副作用:修改數據庫
preferences.edit().putString("last_user", user.name).apply() // 副作用:修改SharedPreferences
analytics.trackEvent("user_saved") // 副作用:發送分析事件
return true
} catch (e: Exception) {
Log.e("UserManager", "Failed to save user", e) // 副作用:日誌輸出
return false
}
}
2.2.3 參數唯一決定返回值(記憶模式)
純函數的一個重要特性是可以緩存其結果,這就是"記憶模式":
// 記憶化的斐波那契函數
class MemoizedFibonacci {
private val cache = mutableMapOf<Int, Long>()
fun fibonacci(n: Int): Long {
// 如果結果已經計算過,直接返回緩存的值
return cache.getOrPut(n) {
when (n) {
0 -> 0
1 -> 1
else -> fibonacci(n - 1) + fibonacci(n - 2)
}
}
}
}
Android UI中的應用:
// 在Jetpack Compose中的memoization
@Composable
fun ExpensiveUI(data: ComplexData) {
// remember函數基於傳入的鍵值緩存結果
// 只有當data變化時才會重新計算
val processedResult = remember(data) {
expensiveComputation(data)
}
Text(text = "Result: $processedResult")
}
2.2.4 必須有返回值
在函數式編程中,每個函數都應該有返回值。即使是控制結構(如if、when)在Kotlin中也被設計為表達式:
// if作為表達式有返回值
val max = if (a > b) a else b
// when作為表達式有返回值
val stringRepresentation = when (color) {
Color.RED -> "Red"
Color.GREEN -> "Green"
Color.BLUE -> "Blue"
else -> "Unknown"
}
在Android開發中的應用:
// UI狀態轉換
val uiState = when {
isLoading -> UiState.Loading
error != null -> UiState.Error(error)
data != null -> UiState.Success(data)
else -> UiState.Empty
}
// 基於UI狀態渲染不同內容
binding.contentView.isVisible = uiState is UiState.Success
binding.errorView.isVisible = uiState is UiState.Error
binding.loadingView.isVisible = uiState is UiState.Loading
binding.emptyView.isVisible = uiState is UiState.Empty
2.3 純函數的好處
2.3.1 測試簡單
純函數非常容易測試,因為輸入確定,輸出也確定:
// 很容易測試的純函數
fun calculateDiscount(price: Double, discountRate: Double): Double {
return price * discountRate
}
// 單元測試
@Test
fun `calculateDiscount correctly applies discount rate`() {
// 給定固定輸入,輸出總是確定的
assertEquals(20.0, calculateDiscount(100.0, 0.2), 0.001)
assertEquals(50.0, calculateDiscount(100.0, 0.5), 0.001)
assertEquals(0.0, calculateDiscount(100.0, 0.0), 0.001)
}
2.3.2 行為可預測
函數簽名可以清晰表達函數的行為:
// 函數簽名清晰表明行為和依賴
fun combineUserData(
profile: UserProfile,
settings: UserSettings
): UserData {
// 從簽名就可以看出,這個函數只依賴傳入的兩個參數
return UserData(
id = profile.id,
name = profile.name,
email = profile.email,
preferences = settings.preferences
)
}
2.3.3 方便優化
編譯器和運行時可以對純函數進行各種優化,如並行執行、緩存結果等。
2.4 函數第一性
2.4.1 一切皆為函數
在函數式編程中,函數是"一等公民",可以像值一樣被傳遞和操作:
// 函數作為參數傳遞
fun processItems(items: List<Int>, processor: (Int) -> Int): List<Int> {
return items.map(processor)
}
// 使用不同的處理函數
val doubled = processItems(listOf(1, 2, 3)) { it * 2 } // [2, 4, 6]
val squared = processItems(listOf(1, 2, 3)) { it * it } // [1, 4, 9]
在Android中的實際應用:
// RecyclerView中的列表項點擊處理
interface ItemClickListener {
fun onItemClick(item: ListItem)
}
// 函數式方式更簡潔
class ItemsAdapter(
private val items: List<ListItem>,
private val onItemClick: (ListItem) -> Unit // 函數類型作為參數
) : RecyclerView.Adapter<ItemViewHolder>() {
// ...
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
holder.itemView.setOnClickListener { onItemClick(item) }
}
}
// 使用時
val adapter = ItemsAdapter(items) { clickedItem ->
// 處理點擊事件
navigateToDetail(clickedItem.id)
}
2.4.2 惰性計算
惰性計算是隻在需要結果時才執行計算的技術。Kotlin通過Sequence提供了惰性計算的支持:
// 熱切計算 - 立即執行所有操作
val result = listOf(1, 2, 3, 4, 5)
.map { println("Map: $it"); it * 2 }
.filter { println("Filter: $it"); it > 5 }
.first()
// 輸出所有map操作,然後所有filter操作
// 惰性計算 - 按需執行
val result = sequenceOf(1, 2, 3, 4, 5)
.map { println("Map: $it"); it * 2 }
.filter { println("Filter: $it"); it > 5 }
.first()
// 只輸出需要的操作:Map: 1, Filter: 2, Map: 2, Filter: 4, Map: 3, Filter: 6
一個完整的斐波那契數列生成器例子:
// 生成無限的斐波那契數列
fun fibonacciSequence(): Sequence<Long> = sequence {
var a = 0L
var b = 1L
// 返回第一個數字 0
yield(a)
// 返回第二個數字 1
yield(b)
// 生成後續的斐波那契數
while (true) {
val next = a + b
yield(next)
// 更新值以計算下一個數字
a = b
b = next
}
}
// 使用時只計算需要的部分
val first10Fibonacci = fibonacciSequence().take(10).toList()
println(first10Fibonacci) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
在Android中的應用:
// 處理大型數據集
fun processLargeLogFile(context: Context, logUri: Uri): Sequence<LogEntry> {
return sequence {
context.contentResolver.openInputStream(logUri)?.use { stream ->
BufferedReader(InputStreamReader(stream)).use { reader ->
// 逐行讀取並處理日誌文件
var line = reader.readLine()
while (line != null) {
// 解析日誌行
val entry = parseLine(line)
// 只在需要時才解析併產生下一個結果
yield(entry)
// 讀取下一行
line = reader.readLine()
}
}
}
}
}
// 使用時可以高效處理
processLargeLogFile(context, logUri)
.filter { it.level == LogLevel.ERROR }
.take(10)
.forEach { showErrorLog(it) }
2.5 異常處理
函數式編程中,異常也被看作值的一部分,而不是特殊的控制流:
// 傳統的異常處理
fun parseJson(json: String): User {
try {
return jsonParser.parse(json)
} catch (e: Exception) {
Log.e("Parser", "Failed to parse", e)
throw e // 或返回默認值
}
}
// 函數式的錯誤處理 - 使用Result
fun parseJson(json: String): Result<User> {
return runCatching {
jsonParser.parse(json)
}
}
// 使用
parseJson(jsonString)
.onSuccess { user ->
// 處理成功情況
displayUser(user)
}
.onFailure { error ->
// 處理錯誤情況
showError(error.message)
}
另一個例子,使用Either類型(在Arrow庫中可用):
// 使用Either類型處理錯誤
sealed class ParseError {
data class InvalidFormat(val message: String) : ParseError()
data class MissingField(val field: String) : ParseError()
data class NetworkError(val code: Int) : ParseError()
}
// 返回Either<ParseError, User>類型
fun parseUser(json: String): Either<ParseError, User> {
// 如果解析成功,返回Right(user)
// 如果有錯誤,返回Left(error)
}
// 使用
when (val result = parseUser(jsonString)) {
is Either.Left -> {
// 處理錯誤
when (val error = result.value) {
is ParseError.InvalidFormat -> showFormatError(error.message)
is ParseError.MissingField -> promptForField(error.field)
is ParseError.NetworkError -> retryWithBackoff(error.code)
}
}
is Either.Right -> {
// 處理成功情況
val user = result.value
displayUser(user)
}
}
2.6 循環、遞歸與尾遞歸
遞歸是函數式編程中替代循環的主要方式,因為它不需要使用變量:
// 使用變量的迭代方式計算階乘
fun factorial(n: Int): Long {
var result = 1L
for (i in 1..n) {
result *= i
}
return result
}
// 使用遞歸計算階乘
fun factorialRecursive(n: Int): Long {
return if (n <= 1) 1 else n * factorialRecursive(n - 1)
}
但遞歸可能導致棧溢出,Kotlin提供了尾遞歸優化:
// 使用尾遞歸優化的階乘計算
tailrec fun factorialTail(n: Int, acc: Long = 1): Long {
return if (n <= 1) acc else factorialTail(n - 1, n * acc)
}
我們可以用尾遞歸重寫斐波那契函數:
// 尾遞歸版本的斐波那契函數
tailrec fun fibonacciNext(a: Long, b: Long, ordinal: Int): Long =
if (ordinal == 1)
a + b
else
fibonacciNext(b, a + b, ordinal - 1)
fun fibonacci(ordinal: Int): Long =
when (ordinal) {
1 -> 0L
2 -> 1L
else -> fibonacciNext(0L, 1L, ordinal - 2)
}
Y組合子簡介
Y組合子是函數式編程中一個高級概念,它允許在沒有顯式遞歸能力的純函數式語言中實現遞歸:
// Y組合子類型定義
typealias RecursiveFunc<T, R> = ((T) -> R) -> (T) -> R
// Y組合子實現
fun <T, R> Y(f: RecursiveFunc<T, R>): (T) -> R {
val g: ((T) -> R, T) -> R = { func, x -> f { y -> func(y) }(x) }
return { x ->
g(run {
fun h(n: T): R = g(::h, n)
::h
}, x)
}
}
// 使用Y組合子實現斐波那契
val fibonacci = Y<Int, Long> { f ->
{ n ->
when (n) {
0 -> 0L
1 -> 1L
else -> f(n - 1) + f(n - 2)
}
}
}
// 使用
println(fibonacci(10)) // 55
這個概念在實際Android開發中用得比較少,但理解它有助於加深對函數式編程本質的理解。