C#中的func<>

c# 里面的 func<> 是什么?

C# Func<>

Func<TResult> 是**.NET 内置的泛型委托** ,用来封装有返回值的方法;与之对应:

  • Func:有返回值
  • Action:无返回值(void)

委托:可以把方法当成变量来传递的类型。

重载版本

Func最多支持16 个输入参数最后一个泛型参数永远是返回值类型

复制代码
// 无参数,返回 TResult
public delegate TResult Func<TResult>();

// 1个输入参数T1,返回TResult
public delegate TResult Func<T1, TResult>(T1 arg1);

// 2个输入参数
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);

// ...一直到 Func<T1,...,T16,TResult>

✅记忆:最后一个类型是返回值,前面全是入参

简单示例

1. 普通方法赋值给 Func

复制代码
// Func<int, int>:输入int,返回int
Func<int, int> square = CalcSquare;

int res = square(5); // 调用,res=25

int CalcSquare(int x)
{
    return x * x;
}

2. 搭配 Lambda(最常用)

日常几乎都是配合 lambda 简写,不用单独写方法:

复制代码
Func<int, int> square = x => x * x;
Console.WriteLine(square(6)); //36

//两个入参,返回bool
Func<int, int, bool> isBigger = (a, b) => a > b;
Console.WriteLine(isBigger(10,2)); //True

//无参数有返回值
Func<string> getMsg = () => "hello func";
Console.WriteLine(getMsg());

和 Action 的对比

表格

类型 是否有返回值 示例
Func<TResult> ✅有返回值 Func<int> f=()=>10;
Action ❌void 无返回 Action a=()=>Console.WriteLine(1);

常见使用场景

  1. LINQ:Where、Select 接收的就是 Func 委托

    var list = new List{1,2,3,4};
    // Where接收 Func<int,bool>
    var r = list.Where(x => x>2);

  2. 回调函数、策略模式,把一段逻辑当参数传入方法

    //接收一个Func做计算逻辑
    static int DoWork(Func<int,int> op, int val)
    {
    return op(val);
    }

    //调用
    int v = DoWork(x=>x*x,7);

  3. 延迟执行:把逻辑包装起来,后面再执行 func()

容易踩坑点

  1. 最后一个泛型是返回值,不要写反

    //错误!以为<int,int>是两个int入参,实际第一个int是返回值,没有入参
    Func<int,int> bad;
    // ✔ 第一个int入参,第二个int返回
    Func<int, int> ok;

  2. Func不能返回 void;要 void 就用Action

    // Func<int,void> 编译报错!void不能做泛型参数
    Action act = x=>Console.WriteLine(x); //正确

  3. 闭包陷阱:lambda 捕获外部变量,是捕获变量本身不是当时的值。

小结

Func<...> = 可以存带返回值方法的泛型委托,最后一个泛型参数代表返回类型,广泛用于 Lambda、LINQ、回调。

如果你需要,我可以给你对比 PredicateFuncAction三者区别。


相当于 delegate 的语法糖?

Func<> 是不是 delegate 的语法糖?

不完全是语法糖,它本身就是编译器预定义好的泛型 delegate 类型。

本质

复制代码
public delegate TResult Func<TResult>();
public delegate TResult Func<T1,TResult>(T1 arg1);

FuncAction 就是 .NET 类库里面已经帮你写好的 delegate 声明,不是 C# 编译器给你做的语法转换糖。

语法糖:编译器看到简写,自动帮你生成 delegate 代码,比如 => lambda 是语法糖。 Func<>:是 BCL 已经定义好的委托类型,你直接拿来用。

对比

  1. 自己手写 delegate:

    // 自己声明一个委托类型
    public delegate int MyCalc(int x);

  2. 使用内置 Func,效果逻辑等价,不用自己写 delegate 声明:

    Func<int,int> calc;

Func<int,int> 和上面手写的 MyCalc 是两个完全不同的委托类型,虽然签名一模一样,但是类型不一样,不能直接互相赋值,需要转换。

复制代码
MyCalc d1 = x=>x*2;
Func<int,int> d2 = d1; // 编译报错,类型不同!

哪怕签名相同,不同 delegate 类型属于不同类型,不能隐式转换。这一点很关键。

理清概念分层

  1. delegate :关键字,用来声明委托类型
  2. Func<> / Action<> :BCL 预先用 delegate 关键字写好的一堆泛型委托类型,直接复用。
  3. Lambda x=>... :这才是语法糖,编译器把 lambda 生成匿名方法,可以赋值给匹配签名的委托(可以赋值给 Func、Action、也可以赋值给你自己手写 delegate)。

通俗总结

  • delegate:造委托类型的模具
  • Func<>:微软已经用模具造好一批现成委托类型,直接用,不用你再拿模具造
  • lambda:写委托实例的简写语法糖

Predicate 补充

Predicate<T> 也是预定义委托:delegate bool Predicate<T>(T obj),等价于 Func<T,bool>,但是独立类型,二者也不能直接互赋值。

小例子区分

复制代码
//1.自己造委托类型
delegate int MyDel(int a);

//2.使用BCL现成委托类型Func
Func<int, int> funcDel;

//3.lambda语法糖,赋值给任意匹配签名委托
MyDel d1 = x => x + 1;
Func<int, int> d2 = x => x + 1;

// d1 = d2; //编译错误!类型不一样
// d2 = d1; //编译错误!类型不一样

//只能通过lambda/匿名方法中转
Func<int,int> d3 = d1.Invoke;

关键点记忆

Func不是简化 delegate 声明的语法糖,是现成 delegate 类型;lambda 才是语法糖。 签名一样,不同委托类型仍然是不同类型,不能隐式互换。

如果你需要,我可以讲下匿名方法、lambda、Func、自定义 delegate 完整演进对比。


delegate 范围比 func 大?大在哪里

delegate 范围比 Func<> 大在哪里

一句话: delegate 是 C# 关键字,用来定义任意委托类型;Func<>只是.NET 预先定义好的一小部分委托实例。delegate能做的,Func 不一定能做;Func 只是 delegate 的子集。

类比: delegate = class 关键字(用来定义类) Func<> = .NET 已经写好的某个类,你直接 new 使用。

1. Func 有硬性限制

Func 家族全部遵循:

  • 返回值不能是 void;void 交给 Action
  • 最多 16 个泛型参数Func<T1...T16,TResult>
  • 返回值必须放在最后一个泛型位置

也就是说:Func 只能表达:0‑16 个参数 + 带返回值 的方法签名。

delegate 没有这些限制,这就是它更大的地方

① 可以定义 ref /out 参数的委托,Func 做不到

BCL 内置的 Func / Action 全部不支持 ref、out 。 如果你需要委托带 out/ref,只能自己 delegate

复制代码
// ✅自定义delegate支持 out
delegate bool TryParseDel(string s, out int val);

// ❌ Func<string, int, bool> 无法表达 out int,没有语法可以标记out

Func 泛型参数只能代表类型,不能修饰参数修饰符 ref/out/in。这是最常用的差距。

② 可以定义返回 void,也可以自定义语义名称

Func 不能 void 返回;void 用 Action。 但自定义 delegate 可以给委托起业务含义名字:

复制代码
// 语义清晰:代表事件回调
public delegate void DataChangedEventHandler(object sender, DataChangedArgs e);

Action<object, DataChangedArgs> 也能装,但名字看不出业务含义。

③ 可以超过 16 个参数

Func 最多 16 个入参。 如果真的需要 17 个参数的委托,只能手写 delegate。(现实极少遇到)

④ 可以定义静态委托、绑定到实例,还能用于事件

C# 的 event 事件底层依赖 delegate 类型

你可以把事件声明为自定义 delegate; 虽然现在也常用 EventHandler<T>(也是内置 delegate),但不能直接把 event 声明成 Func

复制代码
// 合法
public event DataChangedEventHandler OnDataChange;

// ❌编译报错,event不能用Func<>
public event Func<int> OnDataChange;

事件语法要求委托类型,Func 是委托类型,但C# 语法禁止 event 使用 Func/Action,因为事件设计初衷是多播回调,Func 有返回值会产生歧义(多个返回值取哪个?)。

⑤ 可以定义泛型约束,Func 是固定写死的

自定义 delegate 可以加泛型约束:

复制代码
delegate T CreateDel<T>() where T : new();

内置 Func 是 BCL 已经写死,你不能修改它的泛型约束。

范围关系图

复制代码
delegate关键字(可以定义任意委托类型)
├─ 自己手写 delegate MyDel(...)   【完全自由:ref/out、命名、约束、事件】
└─ .NET内置预定义委托
    ├─ Func<T...>      :0‑16参数,有返回值,不能ref/out
    ├─ Action<T...>    :0‑16参数,void返回,不能ref/out
    └─ Predicate<T>、EventHandler<T> 等

关键误区纠正

"Func 是 delegate" ------ 这句话对:Func 本质就是用 delegate 关键字声明出来的类型。 "delegate 就是 Func" ------ 错:delegate 是定义工具,Func 是工具产出的其中一个成品。

什么时候用哪个?

  1. 普通回调、LINQ、lambda:优先 Func / Action,省事。
  2. 需要 ref/out 参数:必须手写 delegate
  3. 写事件:优先 EventHandler<T>,不要 Func/Action。
  4. 需要业务语义名称、泛型约束:自定义 delegate。

补充小坑

即便签名完全一致:

复制代码
delegate int MyDel(int x);
Func<int, int> f;

MyDelFunc<int,int> 是两个完全不同的委托类型,不能隐式转换,只能通过 lambda 或者.Invoke中转。

如果你需要,我可以举个 ref 参数委托完整可运行示例。


func 是委托的语法糖?

结论先说

Func<> 不是委托的语法糖。

  • delegate 是关键字,用来定义委托类型
  • Func<T1,TResult>.NET BCL 已经用 delegate 定义好的委托类型,是一个实实在在的类型,不是编译器帮你偷偷生成的简写。
  • Lambda x=>x+1 才是语法糖

拆开看

这个是源码级别的定义,.NET 内部就是这么写的:

复制代码
public delegate TResult Func<TResult>();
public delegate TResult Func<T1, TResult>(T1 arg1);

这一行是类库代码 ,不是编译器看到你写 Func<> 之后临时生成出来的。

什么叫语法糖?

语法糖:你写简短写法,C# 编译器在编译阶段自动帮你展开成等价代码,源码里不存在那个简写对应的类型。

举例:lambda 就是语法糖:

复制代码
Func<int,int> f = x => x * 2;

编译器会把这个 lambda 翻译成匿名方法,再赋值给委托对象。这是糖。

Func<int,int> 本身不是糖,它就是一个已经存在的委托类型,就跟你自己写:

复制代码
public delegate int MyDelegate(int x);

产出的 MyDelegate 地位完全一样:都是委托类型,只是一个是微软写好放库里面,一个是你自己写。


很多人会混淆的点

"用 Func 就不用手写 delegate 了,这不就是语法糖吗?"

不是语法糖,这只是复用现成类型,省去你手动声明委托类型的工作量

关键证据:

  1. 相同签名,不同委托类型不能隐式转换

    delegate int MyDel(int x);

    MyDel d1 = x=>1;
    Func<int,int> d2 = d1; // ❌编译报错,类型不一样

如果 Func 只是语法糖,编译器应该把二者视为同一类型,但实际不是。它们是两个独立的委托类型。

  1. Func 有硬边界:不支持 ref/out/in 参数,最多 16 个参数。 如果它是编译器语法糖,理论上编译器可以帮你生成支持 ref/out 的版本,但它做不到,因为类型是库里面写死的。

层级关系梳理

  1. delegate 关键字:制造委托类型的工具
  2. Func/Action:工具已经造好的一批现成委托类型(BCL 内置)
  3. Lambda =>语法糖,编译器生成匿名方法,可赋值给任意匹配签名的委托类型(自定义 delegate、Func、Action 都可以接)

类比:

  • delegate 好比 struct 关键字
  • DateTime 是 .NET 内置的 struct,你直接用
  • 你不能说 DateTime 是 struct 的语法糖,它只是一个现成结构体。同理 Func 不是 delegate 的语法糖。

一句话区分记忆

  • ✅ Lambda =>语法糖
  • ✅ Func<>:预定义的委托类型,不是语法糖
  • ✅ delegate:定义委托类型的关键字

如果你愿意,我可以再对比匿名方法、lambda、Func、自定义 delegate,看一段代码编译后 IL,直观看出差异。


就是 func 可以系统自己识别你的委托类型,而 delegate 要自己设置

你的这个感受非常贴近日常写代码的直觉,但表述要修正一下:

❌不是系统 "识别你的委托类型" ✅是 Func<> 是已经存在的现成委托类型,你直接填泛型参数就能用;delegate 关键字是让你全新定义一个委托类型

对比举例

用 delegate:你要先造类型,再用

复制代码
// 第一步:手动定义一个全新的委托类型 MyCalc
delegate int MyCalc(int x);

// 第二步:再去声明变量
MyCalc fn = x => x * 2;

MyCalc 是你创造出来的新类型,程序集里会多出这个类型。

用 Func<>:类型早就有了,你只需要填类型参数

复制代码
// 没有定义新类型!Func<int,int>这个类型本来就在BCL里
Func<int, int> fn = x => x * 2;

你并没有创造新委托类型,只是实例化那个已经存在的泛型委托。


关键误区:它不会自动识别你的方法然后生成委托

很多人以为:我写个 lambda,编译器看我入参返回,自动生成 Func。 不是。 是你显式写了 Func<int,int>,编译器才知道要把 lambda 赋值给这个已有的委托类型。

复制代码
// 这行单独写,编译报错!编译器猜不出来你要什么委托类型
var fn = x => x * 2; 

var fn = x=>x*2; 直接报错,lambda 本身没有类型,必须赋值给一个明确的委托类型,可以是:

  • 现成的 Func<int,int>
  • 或者你自己 delegate 定义的 MyCalc

lambda 本身是 "逻辑片段",没有自身类型,必须挂靠到某一个委托类型上。


通俗大白话总结你的理解

你的原话:func 可以系统自己识别你的委托类型,而 delegate 要自己设置

修正版:

Func 是系统预先写好一批委托类型,你只要填泛型参数就拿来用,不用自己新建委托类型; delegate 关键字是用来从零新建一个属于你自己的委托类型。 两者都需要显式指定类型,Func 不会自动推断生成委托。

核心差异点

  1. delegate A(...)定义新委托类型,输出一个新类型到程序集
  2. Func<TIn,TOut>使用已经存在的委托类型,不产生新类型
  3. Lambda:无类型,只能赋值给上面两种委托类型之一

再强调那个坑

哪怕签名一模一样:

复制代码
delegate int MyCalc(int x);
MyCalc a = x=>x+1;
Func<int,int> b = a; // 编译报错!类型不一样,不能直接赋值

虽然方法签名完全匹配,但MyCalcFunc<int,int>是两个完全不同的类型,不能隐式转换。 如果 Func 是 "自动识别生成",那这行就应该能通过,但现实不行。


什么时候选哪个

  • 普通回调、LINQ:直接 Func / Action,不用自己定义委托类型,省事。
  • 需要 out/ref 参数、写事件、想要业务类型名字:必须自己 delegate 定义新委托类型。

如果你想看,我可以简单讲下 IL 层面,看自定义 delegate 会多出一个类型,而 Func 不会。


这不是一样吗你对比举例的两个

表面写出来的代码看着效果行为一样 :都能存一个 int→int 的方法,调用起来结果一样。 但是类型层面完全不一样,这就是很多人迷惑的根源。

看两段代码:

复制代码
//版本A:自己delegate
delegate int MyCalc(int x);
MyCalc fnA = x => x * 2;
int resA = fnA(5);

//版本B:用Func
Func<int, int> fnB = x => x * 2;
int resB = fnB(5);

运行结果:resA=10,resB=10执行效果一模一样

所以你感觉:这不是一样吗? ✔运行行为一样但是类型、编译产物、能否互相赋值完全不一样


核心差别:编译之后程序集里面发生了什么

A 方案(自定义 delegate)

复制代码
delegate int MyCalc(int x);

编译器会在你的 dll/exe 里生成一个全新的类型:MyCalc ,继承自 MulticastDelegate。 这是属于你项目的新类型。

B 方案(Func<int,int>)

Func<int,int> 不是你项目生成的 ,它来自系统程序集 System.Private.CoreLib.dll,早就写死在框架里。 你只是引用这个现成类型,你的程序集不会多出任何新委托类型

👉 一个是 "你造一个新盒子";一个是 "拿系统现成的盒子"。两个盒子外形尺寸一模一样(装的方法签名一样),但盒子本身不是同一个东西。

证据:不能互相赋值

复制代码
MyCalc fnA = x => x * 2;
Func<int, int> fnB = fnA; // ❌编译报错!

盒子长得一样,但是商标不一样,不能直接把 A 盒子的东西交给 B 盒子变量。

如果它们真的 "完全一样",上面这行就应该编译通过,实际不行。


那为什么 lambda 又可以同时赋值给两者?

复制代码
MyCalc fnA = x=>x*2;
Func<int,int> fnB = x=>x*2;

lambda 本身只是一段逻辑,编译器根据左边变量的委托类型,帮你包装成对应委托对象

  • 左边是MyCalc → 包装成MyCalc实例
  • 左边是Func<int,int> → 包装成Func<int,int>实例

lambda 不是生成 Func,它适配左边是什么委托,就生成什么委托实例。


打个生活化比喻

  • delegate int MyCalc(int x):你自己动手定做一个盒子,规格:输入 int,输出 int,这个盒子归你的项目。
  • Func<int,int>:商店(.NET 框架)已经量产好一模一样规格的盒子,你直接买回来用。

两个盒子装东西的规则完全相同,放进去 5,出来 10。 但是:

  1. 来源不一样:一个你造,一个系统造
  2. 类型身份不一样:不能把定做盒子直接交给商店盒子的变量接收
  3. 定做盒子你可以改规格:比如支持out/ref;商店量产的 Func 盒子规格固定死改不了。

总结你的困惑

"代码写出来效果一样,那到底差别在哪?"

  • 运行调用逻辑:相同
  • CLR 类型身份:不同
  • 编译输出:自定义 delegate 会新增类型;Func 复用系统已有类型
  • 能力边界:自定义 delegate 可以 ref/out;Func 做不到

日常业务写回调,绝大多数场景你感知不到差异,所以很多人误以为 Func 就是 delegate 的简写语法糖。但底层类型模型上二者不能等同。

如果你想看,我可以用伪 IL 展示两者编译后的差别。


func 只能定义 int 类型盒子?

不是,Func<>盒子不是只能 int,泛型可以填任意类型。

复制代码
//输入string,返回bool
Func<string, bool> f1 = s => s.Length > 0;

//无参数,返回List<int>
Func<List<int>> f2 = () => new List<int>();

//两个输入:string,int,返回DateTime
Func<string, int, DateTime> f3 = (str, num) => DateTime.Now;

盒子的入参、返回值全部由你填的泛型参数决定,string、class、自定义类、结构体都可以。


但是!盒子的结构是固定死的(重点)

Func 这个 "盒子模具" 是微软写死的:

Func[T1,T2,...,TResult]

  • 前面全部是输入参数的类型
  • 最后一个永远是返回值类型
  • 只能写类型,不能加修饰符:ref /out/in

✅可以随便换什么类型 ❌不能修改参数的修饰符,这是模具本身的限制。

举例子,Func 做不到的

我想要一个委托:接收 out int 参数

复制代码
// 设想,但是 C# 不支持!泛型占位符不能标记 out
Func<string, out int, bool> // ❌语法错误

泛型参数只代表 "是什么类型",不能写 out/ref这种场景,就必须自己用 delegate 造新盒子

复制代码
delegate bool MyTryParse(string s, out int value);

这个自己造的盒子,我可以自由加 out/ref/in,这是 Func 量产盒子做不到的。


再回顾比喻

  • Func<>:商店量产的一批盒子。
    • 盒子的内部卡槽结构固定:若干输入槽,一个输出槽;卡槽只能放类型,不支持 ref/out 卡扣。
    • 但是卡槽里放什么类型(int、string、自定义类)由你自己选。
  • 手写 delegate:你自己开模做盒子。
    • 卡槽数量你定,还可以额外加上 ref/out 卡扣。卡槽放什么类型也你定。

Func 限制的不是里面放什么数据类型 ,而是盒子本身的参数修饰语法结构

再看一个自定义类示例,Func 完全能用

复制代码
public class User { public string Name; }

//输入User对象,返回string
Func<User, string> getName = u => u.Name;

没问题,随便用自定义 class。

Func 还有一个硬限制

最多16 个输入参数Func<T1,T2...T16,TResult>,超过 16 个入参,Func 这套模具就没有了,只能手写 delegate。


极简小结

  1. Func 泛型可以填任意数据类型,不限于 int。
  2. 限制不在类型,在于参数修饰符 ref/out/in 不支持、最多 16 入参
  3. 一旦需要 ref/out,只能 delegate 自己造盒子。

delegate 和 func 分别支持的数据类型帮我归纳下

delegate vs Func<> 支持的数据 / 参数能力归纳

注意:这里说的不是能传什么数据类型(int/string/class 都支持) ,而是参数修饰符、参数数量、返回值形式,普通数据类型两者全都支持。

1️⃣ Func<>(系统预定义委托)

底层:public delegate TResult Func<T1,T2,...,TResult>(T1 arg1, T2 arg2...)

表格

项目 Func<>
普通值类型 ✅ int、bool、DateTime、struct 全部支持
引用类型 ✅ string、自定义 class、List<T> 全部支持
返回值 必须有返回值,不能 void;void 请用 Action
参数数量 ✅ 0~16 个输入参数;超过 16 个❌不支持
ref 参数 完全不支持
out 参数 完全不支持
in 参数 完全不支持
泛型约束 ❌不能自定义约束,框架已经写死
用于 event 事件 ❌语法禁止使用 Func 做事件类型
自定义委托名称 ❌不能起业务语义名字,固定叫 Func

关键点:Func 的泛型占位符只能描述是什么类型,无法携带 ref/out/in 修饰符。

复制代码
Func<string, int, bool> ok;           //正常
Func<string, out int, bool> error;    //语法报错,不允许写out

2️⃣ delegate 关键字(自定义委托)

delegate 是用来定义新委托类型的关键字,你自己写签名。

表格

项目 自定义 delegate
普通值类型 ✅ int、bool、struct 全部支持
引用类型 ✅ string、自定义 class 全部支持
返回值 ✅ 可以返回任意类型,也可以返回 void
参数数量 ✅ 理论无上限,写多少个参数都行
ref 参数 ✅ 原生支持
out 参数 ✅ 原生支持
in 参数 ✅ 原生支持
泛型约束 ✅ 可以写 where T:xxx 泛型约束
用于 event 事件 ✅ 完全支持,事件标准用法
自定义委托名称 ✅ 可以给委托起业务名字,例如TryParseHandler

示例:

复制代码
// out、ref、void、泛型约束全都可以
public delegate bool TryHandler<T>(string input, out T result) where T : struct;

3️⃣ 一张表看清哪些场景该选谁

表格

场景 选 Func/Action 必须手写 delegate
普通回调、LINQ,无 ref/out
回调需要 out /ref/in 参数
写事件 (event) ✅(优先 EventHandler<T>)
入参超过 16 个
想要委托带业务语义名称
需要自定义泛型约束

4️⃣容易混淆的点澄清

  1. int、string、自定义类,两种都可以用,没有区别 限制不在于你传什么数据,而在于参数的修饰符
  2. Func 本质上也是 delegate,是微软用 delegate 关键字定义好的一批委托;只是它的签名模板固定死了,不支持 ref/out。
  3. Action:Func 的兄弟,无返回值,同样不支持 ref/out/in。

极简记忆口诀

Func/Action:类型随便填,ref/out 不能玩;最多 16 参数,事件不能干。 自定义 delegate:签名我做主,ref/out 随便撸;事件约束泛型,全部都能铺。

相关推荐
highreport1 小时前
net报表工具对比:HighReport 与 FastReport
java·c#
BillKu1 小时前
vue3 字符串排序 localeCompare,确保排序为 “B01“, “B10“, “B2“
前端·javascript·vue.js
xiaoqiMikko1 小时前
七条 advisory 各给了一个修复版,而一次修完的那个数字一条都没写
java·spring boot
vHelios1 小时前
【电商项目】短信测试遇到的问题与解决方案:InaccessibleObjectException报错、单元测试通过但未接收到验证码
java·微服务
一鸣AI编程1 小时前
功能用例接入 TestStar AI UI:一份可执行的接入清单
前端
晴天161 小时前
打造自己的 npm 包实战指南
前端·npm·node.js
气泡水好喝1 小时前
Java魔法探秘:从Java代码到CPU指令,一个方法调用的完整生命周期
java
然我1 小时前
从 Service 到生命周期:Agent Runtime 的插件内核
前端·javascript·agent
气泡水好喝1 小时前
读《阿里巴巴Java开发手册》五年,这7条规约救了我太多次
java