函数式编程在C#中的应用与实践
1. 函数式编程简介
函数式编程是一种编程范式,它将计算视为数学函数的求值,并避免了命令式编程中常见的状态和可变数据。这种编程方式强调不可变性、纯函数和高阶函数。函数式编程不仅在学术界广受欢迎,而且在工业界也越来越受到重视,尤其是在并发编程和大数据处理领域。C#作为一种多范式编程语言,近年来也逐渐引入了许多函数式编程的特性。
1.1 函数式编程的历史回顾
函数式编程的历史可以追溯到20世纪30年代和40年代,当时阿隆佐·丘奇在其λ演算中定义了函数式编程的核心概念。λ演算是一个用于表达计算的形式系统,它奠定了函数式编程的理论基础。1958年,LISP语言的出现标志着函数式编程的开端。LISP不仅引入了许多至今仍在编程语言中使用的重要概念,还因其简洁性和灵活性成为学术研究的宠儿。
1.2 函数式编程的特性
函数式编程有以下几个显著特性:
- 不可变性 :数据一旦创建就不能被修改,所有操作都返回新的数据结构。
- 纯函数 :函数的返回值仅依赖于输入参数,不依赖于外部状态,也不会引发副作用。
- 高阶函数 :函数可以作为参数传递,也可以作为返回值返回。
- 惰性求值 :表达式只有在需要时才会被求值,这可以提高性能并减少不必要的计算。
2. C#中的函数式编程基础
C#自3.0版本以来引入了许多函数式编程的特性,如Lambda表达式、匿名函数、LINQ等。这些特性使得C#程序员可以更方便地编写函数式代码。下面我们将详细介绍C#中的一些关键函数式编程基础。
2.1 函数、委托和Lambda表达式
在C#中,函数可以被视为第一类公民,这意味着它们可以像其他任何数据类型一样被传递和操作。委托(Delegate)是C#中用于表示函数的类型,而Lambda表达式则提供了一种简洁的方式来定义匿名函数。
2.1.1 Lambda表达式的使用
Lambda表达式是C#中编写匿名函数的一种简便方式。它们通常用于定义短小的、临时的函数逻辑。以下是一个简单的Lambda表达式示例:
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(5, 10)); // 输出 15
Lambda表达式还可以用于更复杂的场景,例如在LINQ查询中:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
2.2 泛型与类型安全
泛型是C#中另一个重要的特性,它允许我们编写类型安全的代码,而不必为每种类型都编写特定的实现。泛型函数和泛型类使得代码更加通用和复用性强。
2.2.1 泛型函数的使用
泛型函数可以接受不同类型参数,并返回相应类型的值。以下是一个泛型函数的示例:
public T Max<T>(T a, T b) where T : IComparable
{
return a.CompareTo(b) > 0 ? a : b;
}
var maxInt = Max(10, 20); // 返回 20
var maxString = Max("apple", "banana"); // 返回 "banana"
2.3 惰性列表与迭代器
惰性求值是函数式编程中的一个重要概念,它允许我们在需要时才计算表达式的值。C#中的迭代器(Iterator)可以通过
yield return
语句实现惰性求值。
2.3.1 惰性列表的实现
以下是一个使用迭代器实现惰性列表的示例:
public static IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
yield return a;
int temp = a;
a = b;
b = temp + b;
}
}
var fib = Fibonacci().Take(10);
foreach (var num in fib)
{
Console.WriteLine(num);
}
3. 实现函数式技术
在C#中实现函数式技术不仅可以提高代码的可读性和可维护性,还能带来性能上的优化。接下来我们将介绍几种常见的函数式技术及其在C#中的实现。
3.1 柯里化和部分应用
柯里化是一种将多参数函数转换为一系列单参数函数的技术。部分应用则是指将函数的部分参数提前绑定,生成一个新的函数。这两种技术在函数模块化和代码重用方面非常有用。
3.1.1 柯里化的实现
以下是一个手动实现柯里化的示例:
Func<int, int, int> add = (x, y) => x + y;
Func<int, Func<int, int>> curryAdd = x => y => x + y;
var add5 = curryAdd(5);
Console.WriteLine(add5(10)); // 输出 15
3.2 惰性求值
惰性求值可以显著提高性能,尤其是在处理大规模数据时。通过延迟计算,我们可以避免不必要的资源消耗。
3.2.1 惰性求值的实现
以下是一个使用惰性求值的示例:
public static IEnumerable<int> Range(int start, int count)
{
for (int i = 0; i < count; i++)
{
yield return start + i;
}
}
var range = Range(1, 1000000);
var sum = range.Sum();
Console.WriteLine(sum); // 输出 500000500000
3.3 缓存技术
缓存技术可以提高代码的性能,尤其是在需要频繁计算相同结果的情况下。记忆化(Memoization)是一种常用的缓存技术,它可以避免重复计算。
3.3.1 记忆化的实现
以下是一个使用记忆化的示例:
public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> func)
{
var cache = new Dictionary<T, TResult>();
return arg =>
{
if (!cache.TryGetValue(arg, out TResult result))
{
result = func(arg);
cache[arg] = result;
}
return result;
};
}
Func<int, int> fibonacci = null;
fibonacci = n => n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
var memoizedFibonacci = fibonacci.Memoize();
Console.WriteLine(memoizedFibonacci(10)); // 输出 55
Console.WriteLine(memoizedFibonacci(20)); // 输出 6765
4. 函数式编程的实际应用
函数式编程不仅限于理论研究,它在实际项目中也有广泛的应用。通过将函数式编程思想融入到日常开发中,我们可以编写出更健壮、更高效的代码。
4.1 集成函数式编程方法
在现有项目中集成函数式编程方法需要一定的技巧和策略。以下是一些建议:
- 重构代码 :逐步将现有代码重构为函数式风格,确保每次修改都能通过单元测试。
- 编写新代码 :在新模块中优先使用函数式编程思想,确保代码的可读性和可维护性。
- 寻找函数式编程的可能候选人 :识别可以应用函数式编程的场景,如并发编程、数据处理等。
4.1.1 使用Windows窗体UI的列表过滤
以下是一个使用函数式编程方法实现列表过滤的示例:
private void InitData(List<Person> data)
{
Action updateUI = delegate
{
DisplayListBox.DataSource = GetFilteredList(data, GetFilterString());
};
FilterButton.Click += delegate
{
updateUI();
};
updateUI();
}
private static List<Person> GetFilteredList(List<Person> source, string filter)
{
return filter == null ? source : (from p in source
where p.Name.Contains(filter)
select p).ToList();
}
4.2 MapReduce模式
MapReduce是一种用于处理大规模数据集的编程模型。它通过将任务分解为映射(Map)和归约(Reduce)两个阶段来实现并行计算。
4.2.1 MapReduce的实现
以下是一个简单的MapReduce实现示例:
public static IEnumerable<Tuple<TKey, TReduceResult>> MapReduce<TKey, TValue, TReduceInput, TReduceResult>(
Converter<TValue, IEnumerable<Tuple<TKey, TReduceInput>>> mapStep,
Func<TReduceResult, Tuple<TKey, TReduceInput>, TReduceResult> reduceStep,
TReduceResult reduceStartVal,
IEnumerable<TValue> list)
{
var pairs = Functional.Collect(mapStep, list);
var groups = Group(pair => pair.Item1, pairs);
return Functional.Map(
g => Tuple.Create(g.Key, Functional.FoldL(reduceStep, reduceStartVal, g.Values)),
groups);
}
5. 应用功能模块化
函数式编程强调模块化和代码复用。通过将复杂问题分解为多个小的、独立的模块,我们可以更轻松地管理和维护代码。
5.1 执行SQL查询的函数式方法
在函数式编程中,我们可以将SQL查询封装为高阶函数,从而提高代码的可读性和可维护性。
5.1.1 SQL查询的函数式实现
以下是一个使用函数式方法执行SQL查询的示例:
static void FillDatabase()
{
using (var conn = new SqlCeConnection(DBCONNSTR))
{
conn.Open();
try
{
using (var trans = conn.BeginTransaction())
{
ExecuteSQL(trans, "create table people(id int, name ntext)");
trans.Commit();
}
using (var trans = conn.BeginTransaction())
{
ExecuteSQL(trans, "insert into people(id, name) values(1, 'Harry')");
ExecuteSQL(trans, "insert into people(id, name) values(2, 'Jane')");
ExecuteSQL(trans, "insert into people(id, name) values(3, 'Willy')");
ExecuteSQL(trans, "insert into people(id, name) values(4, 'Susan')");
ExecuteSQL(trans, "insert into people(id, name) values(5, 'Bill')");
ExecuteSQL(trans, "insert into people(id, name) values(6, 'Jennifer')");
ExecuteSQL(trans, "insert into people(id, name) values(7, 'John')");
ExecuteSQL(trans, "insert into people(id, name) values(8, 'Anna')");
ExecuteSQL(trans, "insert into people(id, name) values(10, 'Mary')");
trans.Commit();
}
}
finally
{
conn.Close();
}
}
}
5.2 优化与查询
在函数式编程中,优化查询和解析数据是常见的任务。通过使用高阶函数和惰性求值,我们可以更高效地处理大规模数据。
5.2.1 查询优化的实现
以下是一个使用惰性求值优化查询的示例:
public static IEnumerable<PointResult> CalcArea(int width, int height, CalcInfo calcInfo)
{
var points = PointSequence(width, height);
return Functional.Map(p => CalcPoint(p, calcInfo), points);
}
public static Image CalcImage(IEnumerable<PointResult> results, Point start, int width, int height)
{
return Functional.FoldL<PointResult, Bitmap>(
(r, v) =>
{
r.SetPixel(v.Point.X, v.Point.Y, v.Color);
return r;
},
new Bitmap(width, height),
results);
}
6. 数据结构与算法
函数式编程中的数据结构和算法设计有其独特之处。不可变数据结构和递归算法是函数式编程中的常见选择。
6.1 不可变数据结构
不可变数据结构确保了数据的一致性和安全性。通过使用不可变数据结构,我们可以避免许多并发编程中的问题。
6.1.1 不可变数据结构的实现
以下是一个不可变队列的实现示例:
public sealed class Queue<T>
{
private readonly List<T> f, r;
public static Queue<T> Empty { get; } = new Queue<T>(new List<T>(), new List<T>());
private Queue(List<T> f, List<T> r)
{
this.f = f;
this.r = r;
}
public static Queue<T> Snoc(Queue<T> q, T e)
{
return CheckBalance(new Queue<T>(q.f, q.r.Cons(e)));
}
public Queue<T> Snoc(T e)
{
return Snoc(this, e);
}
private static Queue<T> CheckBalance(Queue<T> q)
{
if (q.f.Count < q.r.Count)
{
var newFront = q.f.Concat(q.r.Reverse()).ToList();
return new Queue<T>(newFront, new List<T>());
}
return q;
}
}
6.2 递归算法
递归算法是函数式编程中常见的设计模式。通过递归,我们可以更自然地表达某些问题的解决方案。
6.2.1 递归算法的实现
以下是一个使用递归实现斐波那契数列的示例:
public static int Fibonacci(int n)
{
if (n <= 1) return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
6.3 单子
单子是函数式编程中的一个重要概念,它提供了一种处理副作用的抽象方式。通过单子,我们可以更优雅地处理异步操作、错误处理等问题。
6.3.1 单子的实现
以下是一个使用单子模式处理可能失败的操作的示例:
public class Maybe<T>
{
private readonly T value;
private readonly bool hasValue;
private Maybe(T value, bool hasValue)
{
this.value = value;
this.hasValue = hasValue;
}
public static Maybe<T> Empty { get; } = new Maybe<T>(default, false);
public static Maybe<T> Just(T value) => new Maybe<T>(value, true);
public Maybe<TResult> Bind<TResult>(Func<T, Maybe<TResult>> f)
{
if (!hasValue) return Maybe<TResult>.Empty;
return f(value);
}
public T GetValueOrDefault(T defaultValue)
{
return hasValue ? value : defaultValue;
}
}
public static Maybe<int> Divide(int numerator, int denominator)
{
if (denominator == 0) return Maybe<int>.Empty;
return Maybe<int>.Just(numerator / denominator);
}
var result = Divide(10, 2).Bind(x => Divide(x, 5));
Console.WriteLine(result.GetValueOrDefault(0)); // 输出 1
接下来,我们将深入探讨更多高级主题,如并行编程、持久化数据结构等。通过这些内容,希望能够帮助读者更好地理解和应用函数式编程的思想和方法。
7. 并行编程与性能优化
并行编程是现代软件开发中的一个重要领域,特别是在多核处理器普及的今天。函数式编程因其固有的不可变性和纯函数特性,非常适合并行计算。接下来,我们将探讨如何在C#中利用函数式编程进行并行编程,并优化性能。
7.1 并行LINQ (PLINQ)
并行LINQ(PLINQ)是.NET框架提供的一个扩展,它允许我们以声明式的方式编写并行查询。通过使用
AsParallel()
方法,我们可以轻松地将LINQ查询转换为并行执行。
7.1.1 并行LINQ的实现
以下是一个使用PLINQ进行并行计算的示例:
var numbers = Enumerable.Range(0, 20);
var parallelSum = numbers.AsParallel().Sum();
Console.WriteLine(parallelSum); // 输出 190
PLINQ通过自动划分数据集并在多个线程上并行执行查询,极大地提高了性能。此外,PLINQ还提供了许多其他并行操作,如
Select
、
Where
、
OrderBy
等。
7.2 Task Parallel Library (TPL)
Task Parallel Library (TPL)是.NET框架中用于并行编程的核心库。它提供了丰富的API来创建和管理任务,从而实现高效的并行计算。
7.2.1 TPL的实现
以下是一个使用TPL进行并行计算的示例:
var tasks = new Task<int>[10];
for (int i = 0; i < 10; i++)
{
int taskIndex = i;
tasks[i] = Task.Run(() =>
{
return Calculate(taskIndex);
});
}
Task.WaitAll(tasks);
var results = tasks.Select(t => t.Result).ToArray();
Console.WriteLine($"Results: {string.Join(", ", results)}");
int Calculate(int index)
{
// 模拟耗时计算
Thread.Sleep(1000);
return index * 2;
}
7.3 性能优化
在并行编程中,性能优化是一个关键问题。通过合理的任务划分、减少锁竞争和最小化内存分配,我们可以显著提高并行程序的性能。
7.3.1 性能优化的实现
以下是一些常见的性能优化策略:
- 任务划分 :将大任务划分为多个小任务,以充分利用多核处理器。
- 减少锁竞争 :尽量减少共享资源的使用,避免不必要的锁操作。
- 最小化内存分配 :避免频繁的内存分配和垃圾回收,减少性能开销。
8. 持久化数据结构
持久化数据结构是一种不可变的数据结构,它在更新时不会修改原有数据,而是返回一个新的数据结构。这种方式不仅保证了数据的安全性,还支持高效的并发操作。
8.1 持久化列表
持久化列表是一种常见的持久化数据结构,它支持高效的插入和删除操作。通过使用惰性求值和共享子结构,持久化列表可以在不影响性能的情况下实现不可变性。
8.1.1 持久化列表的实现
以下是一个持久化列表的实现示例:
public sealed class PersistentList<T>
{
private readonly T head;
private readonly PersistentList<T> tail;
private PersistentList(T head, PersistentList<T> tail)
{
this.head = head;
this.tail = tail;
}
public static PersistentList<T> Empty { get; } = new PersistentList<T>(default, null);
public static PersistentList<T> Cons(T head, PersistentList<T> tail)
{
return new PersistentList<T>(head, tail);
}
public T Head => head;
public PersistentList<T> Tail => tail;
public override string ToString()
{
if (this == Empty) return "Empty";
return $"{head}, {tail}";
}
}
var list = PersistentList<int>.Cons(1, PersistentList<int>.Cons(2, PersistentList<int>.Empty));
Console.WriteLine(list); // 输出 1, 2, Empty
8.2 持久化队列
持久化队列是一种支持高效插入和删除操作的不可变数据结构。通过使用双端队列和惰性求值,持久化队列可以在不影响性能的情况下实现不可变性。
8.2.1 持久化队列的实现
以下是一个持久化队列的实现示例:
public sealed class PersistentQueue<T>
{
private readonly List<T> front;
private readonly List<T> rear;
private PersistentQueue(List<T> front, List<T> rear)
{
this.front = front;
this.rear = rear;
}
public static PersistentQueue<T> Empty { get; } = new PersistentQueue<T>(new List<T>(), new List<T>());
public static PersistentQueue<T> Enqueue(T item, PersistentQueue<T> queue)
{
var newRear = queue.rear.Add(item);
return new PersistentQueue<T>(queue.front, newRear);
}
public static PersistentQueue<T> Dequeue(PersistentQueue<T> queue)
{
if (queue.front.IsEmpty())
{
var reversedRear = queue.rear.Reverse().ToList();
return new PersistentQueue<T>(reversedRear.Skip(1).ToList(), new List<T>());
}
return new PersistentQueue<T>(queue.front.Skip(1).ToList(), queue.rear);
}
public T Peek(PersistentQueue<T> queue)
{
if (queue.front.IsEmpty())
{
return queue.rear.Last();
}
return queue.front.First();
}
public bool IsEmpty(PersistentQueue<T> queue)
{
return queue.front.IsEmpty() && queue.rear.IsEmpty();
}
}
var queue = PersistentQueue<int>.Enqueue(1, PersistentQueue<int>.Empty);
queue = PersistentQueue<int>.Enqueue(2, queue);
queue = PersistentQueue<int>.Enqueue(3, queue);
Console.WriteLine(PersistentQueue<int>.Peek(queue)); // 输出 1
queue = PersistentQueue<int>.Dequeue(queue);
Console.WriteLine(PersistentQueue<int>.Peek(queue)); // 输出 2
8.3 持久化平衡二叉树
持久化平衡二叉树是一种支持高效插入、删除和查找操作的不可变数据结构。通过使用惰性求值和共享子结构,持久化平衡二叉树可以在不影响性能的情况下实现不可变性。
8.3.1 持久化平衡二叉树的实现
以下是一个持久化平衡二叉树的实现示例:
public sealed class PersistentBST<T>
{
private readonly T value;
private readonly PersistentBST<T> left;
private readonly PersistentBST<T> right;
private PersistentBST(T value, PersistentBST<T> left, PersistentBST<T> right)
{
this.value = value;
this.left = left;
this.right = right;
}
public static PersistentBST<T> Empty { get; } = null;
public static PersistentBST<T> Insert(T value, PersistentBST<T> tree)
{
if (tree == null)
{
return new PersistentBST<T>(value, null, null);
}
if (Comparer<T>.Default.Compare(value, tree.value) < 0)
{
return new PersistentBST<T>(tree.value, Insert(value, tree.left), tree.right);
}
else
{
return new PersistentBST<T>(tree.value, tree.left, Insert(value, tree.right));
}
}
public bool Contains(T value, PersistentBST<T> tree)
{
if (tree == null)
{
return false;
}
if (Comparer<T>.Default.Compare(value, tree.value) == 0)
{
return true;
}
if (Comparer<T>.Default.Compare(value, tree.value) < 0)
{
return Contains(value, tree.left);
}
else
{
return Contains(value, tree.right);
}
}
public override string ToString()
{
if (this == Empty) return "Empty";
return $"({left}, {value}, {right})";
}
}
var bst = PersistentBST<int>.Insert(10, PersistentBST<int>.Empty);
bst = PersistentBST<int>.Insert(5, bst);
bst = PersistentBST<int>.Insert(15, bst);
Console.WriteLine(bst.Contains(5, bst)); // 输出 True
Console.WriteLine(bst.Contains(20, bst)); // 输出 False
9. 函数式编程的高级主题
函数式编程有许多高级主题,如组合子逻辑、类型类、范畴论等。这些主题不仅丰富了函数式编程的理论基础,还在实际编程中提供了强大的工具。
9.1 组合子逻辑
组合子逻辑是一种基于组合子的逻辑系统,它可以通过组合基本函数来构建复杂的函数。通过使用组合子逻辑,我们可以更简洁地表达函数式编程中的概念。
9.1.1 组合子逻辑的实现
以下是一个使用组合子逻辑的示例:
public static Func<A, C> Compose<A, B, C>(Func<B, C> f, Func<A, B> g)
{
return x => f(g(x));
}
Func<int, int> doubleIt = x => x * 2;
Func<int, int> addOne = x => x + 1;
Func<int, int> composed = Compose(doubleIt, addOne);
Console.WriteLine(composed(5)); // 输出 11
9.2 类型类
类型类是一种用于定义类型之间关系的机制。通过类型类,我们可以为不同类型定义通用的行为,从而实现更灵活的代码。
9.2.1 类型类的实现
以下是一个使用类型类的示例:
public interface IFunctor<F>
{
Func<B> Map<A, B>(Func<A, B> f, Func<A> fa);
}
public class Option<T>
{
private readonly T value;
private readonly bool hasValue;
private Option(T value, bool hasValue)
{
this.value = value;
this.hasValue = hasValue;
}
public static Option<T> Some(T value) => new Option<T>(value, true);
public static Option<T> None() => new Option<T>(default, false);
public T GetValueOrDefault(T defaultValue)
{
return hasValue ? value : defaultValue;
}
}
public class OptionFunctor : IFunctor<Option>
{
public Func<B> Map<A, B>(Func<A, B> f, Func<A> fa)
{
var option = fa();
return () => option.HasValue ? f(option.GetValueOrDefault(default)) : default;
}
}
var functor = new OptionFunctor();
var result = functor.Map<int, string>(x => x.ToString(), () => Option<int>.Some(42));
Console.WriteLine(result()); // 输出 "42"
9.3 范畴论
范畴论是一种研究数学结构和变换的学科。在函数式编程中,范畴论为我们提供了强大的抽象工具,帮助我们更好地理解和设计代码。
9.3.1 范畴论的实现
以下是一个使用范畴论的示例:
public interface IMonoid<T>
{
T Identity { get; }
T Combine(T a, T b);
}
public class IntMonoid : IMonoid<int>
{
public int Identity => 0;
public int Combine(int a, int b) => a + b;
}
public class StringMonoid : IMonoid<string>
{
public string Identity => "";
public string Combine(string a, string b) => a + b;
}
public static T Fold<T>(IMonoid<T> monoid, IEnumerable<T> elements)
{
return elements.Aggregate(monoid.Identity, (acc, elem) => monoid.Combine(acc, elem));
}
var intMonoid = new IntMonoid();
var sum = Fold(intMonoid, new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(sum); // 输出 15
var stringMonoid = new StringMonoid();
var concatenated = Fold(stringMonoid, new[] { "Hello", " ", "World" });
Console.WriteLine(concatenated); // 输出 "Hello World"
10. 函数式编程的未来展望
函数式编程作为一种强大的编程范式,已经在学术界和工业界取得了显著的进展。随着多核处理器的普及和大数据处理需求的增长,函数式编程将继续发挥重要作用。未来,我们可以期待更多创新的函数式编程工具和技术的出现,进一步推动编程范式的演进。
10.1 新兴趋势
- 函数式编程语言的崛起 :越来越多的函数式编程语言如Scala、Elixir等逐渐受到关注,它们为开发者提供了更多的选择。
- 函数式编程与机器学习的结合 :函数式编程的不可变性和纯函数特性非常适合机器学习中的数据处理和模型训练。
- 函数式编程与云计算的融合 :随着云计算的发展,函数式编程的并行计算和分布式处理能力将得到更广泛的应用。
10.2 社区与资源
函数式编程社区非常活跃,有许多优秀的资源可以帮助开发者学习和应用函数式编程思想。以下是一些推荐的资源:
- 书籍 :如《Real World Haskell》、《Functional Programming in Scala》等。
- 在线课程 :如Coursera、Udemy等平台上的函数式编程课程。
- 开源项目 :如Haskell、F#等语言的官方库和工具。
通过不断学习和实践,我们可以更好地掌握函数式编程的思想和方法,为未来的编程挑战做好准备。
函数式编程在C#中的应用与实践不仅展示了其理论上的优越性,也在实际项目中带来了诸多好处。通过掌握这些技术和思想,我们可以编写出更健壮、更高效的代码,迎接未来的编程挑战。希望这篇文章能够帮助读者更好地理解和应用函数式编程,为编程之路增添新的视角和工具。
超级会员免费看

被折叠的 条评论
为什么被折叠?



