動態構造任意複雜的 Linq Where 表達式

前言

       Linq 是 C# 中一個非常好用的集合處理庫,用好了能幫我們簡化大量又臭又長的嵌套循環,使處理邏輯清晰可見。EF 查詢主要也是依賴 Linq。但是 Linq 相對 sql 也存在一些缺點,最主要的就是動態構造查詢的難度。sql 只需要簡單進行字符串拼接,操作難度很低(當然出錯也相當容易),而 Linq 表達式由於對強類型表達式樹的依賴,動態構造查詢表達式基本相當於手寫 AST(抽象語法樹),可以說難度暴增。

       AST 已經進入編譯原理的領域,對計算機系統的瞭解程度需求比一般 crud 寫業務代碼高了幾個量級,也導致很多人覺得 EF 不好用,爲了寫個動態查詢要學編譯原理這個代價還是挺高的。後來也有一些類似 DynamicLinq 的類庫能用表達式字符串寫動態查詢。

       本着學習精神,研究了一段時間,寫了一個在我的想象力範圍內,可以動態構造任意複雜的 Where 表達式的輔助類。這個輔助類的過濾條件使用了 JqGrid 的高級查詢的數據結構,這是我第一個知道能生成複雜嵌套查詢,並且查詢數據使用 json 方便解析的 js 表格插件。可以無縫根據 JqGrid 的高級查詢生成 Where 表達式。

正文

實現

       JqGrid 高級查詢數據結構定義,用來反序列化:

 1     public class JqGridParameter
 2     {
 3         /// <summary>
 4         /// 是否搜索,本來應該是bool,true
 5         /// </summary>
 6         public string _search { get; set; }
 7         /// <summary>
 8         /// 請求發送次數,方便服務器處理重複請求
 9         /// </summary>
10         public long Nd { get; set; }
11         /// <summary>
12         /// 當頁數據條數
13         /// </summary>
14         public int Rows { get; set; }
15         /// <summary>
16         /// 頁碼
17         /// </summary>
18         public int Page { get; set; }
19         /// <summary>
20         /// 排序列,多列排序時爲排序列名+空格+排序方式,多個列之間用逗號隔開。例:id asc,name desc
21         /// </summary>
22         public string Sidx { get; set; }
23         /// <summary>
24         /// 分離後的排序列
25         /// </summary>
26         public string[][] SIdx => Sidx.Split(", ").Select(s => s.Split(" ")).ToArray();
27         /// <summary>
28         /// 排序方式:asc、desc
29         /// </summary>
30         public string Sord { get; set; }
31         /// <summary>
32         /// 高級搜索條件json
33         /// </summary>
34         public string Filters { get; set; }
35 
36         /// <summary>
37         /// 序列化的高級搜索對象
38         /// </summary>
39         public JqGridSearchRuleGroup FilterObject => Filters.IsNullOrWhiteSpace()
40             ? new JqGridSearchRuleGroup { Rules = new[] { new JqGridSearchRule { Op = SearchOper, Data = SearchString, Field = SearchField } } }
41             : JsonSerializer.Deserialize<JqGridSearchRuleGroup>(Filters ?? string.Empty);
42 
43         /// <summary>
44         /// 簡單搜索字段
45         /// </summary>
46         public string SearchField { get; set; }
47         /// <summary>
48         /// 簡單搜索關鍵字
49         /// </summary>
50         public string SearchString { get; set; }
51         /// <summary>
52         /// 簡單搜索操作
53         /// </summary>
54         public string SearchOper { get; set; }
55 
56     }
57 
58     /// <summary>
59     /// 高級搜索條件組
60     /// </summary>
61     public class JqGridSearchRuleGroup
62     {
63         /// <summary>
64         /// 條件組合方式:and、or
65         /// </summary>
66         public string GroupOp { get; set; }
67         /// <summary>
68         /// 搜索條件集合
69         /// </summary>
70         public JqGridSearchRule[] Rules { get; set; }
71         /// <summary>
72         /// 搜索條件組集合
73         /// </summary>
74         public JqGridSearchRuleGroup[] Groups { get; set; }
75     }
76 
77     /// <summary>
78     /// 高級搜索條件
79     /// </summary>
80     public class JqGridSearchRule
81     {
82         /// <summary>
83         /// 搜索字段
84         /// </summary>
85         public string Field { get; set; }
86         /// <summary>
87         /// 搜索字段的大駝峯命名
88         /// </summary>
89         public string PascalField => Field?.Length > 0 ? Field.Substring(0, 1).ToUpper() + Field.Substring(1) : Field;
90         /// <summary>
91         /// 搜索操作
92         /// </summary>
93         public string Op { get; set; }
94         /// <summary>
95         /// 搜索關鍵字
96         /// </summary>
97         public string Data { get; set; }
98     }

       Where 條件生成器,代碼有點多,有點複雜。不過註釋也很多,稍微耐心點應該不難看懂:

  1     /// <summary>
  2     /// JqGrid搜索表達式擴展
  3     /// </summary>
  4     public static class JqGridSearchExtensions
  5     {
  6         //前端的(不)屬於條件搜索需要傳遞一個json數組的字符串作爲參數
  7         //爲了避免在搜索字符串的時候分隔符是搜索內容的一部分導致搜索關鍵字出錯
  8         //無論定義什麼分隔符都不能完全避免這種尷尬的情況,所以使用標準的json以絕後患
  9         /// <summary>
 10         /// 根據搜索條件構造where表達式,支持JqGrid高級搜索
 11         /// </summary>
 12         /// <typeparam name="T">搜索的對象類型</typeparam>
 13         /// <param name="ruleGroup">JqGrid搜索條件組</param>
 14         /// <param name="propertyMap">屬性映射,把搜索規則的名稱映射到屬性名稱,如果屬性是複雜類型,使用點號可以繼續訪問內部屬性</param>
 15         /// <returns>where表達式</returns>
 16         public static Expression<Func<T, bool>> BuildWhere<T>(JqGridSearchRuleGroup ruleGroup, IDictionary<string, string> propertyMap)
 17         {
 18             ParameterExpression parameter = Expression.Parameter(typeof(T), "searchObject");
 19 
 20             return Expression.Lambda<Func<T, bool>>(BuildGroupExpression<T>(ruleGroup, parameter, propertyMap), parameter);
 21         }
 22 
 23         /// <summary>
 24         /// 構造搜索條件組的表達式(一個組中可能包含若干子條件組)
 25         /// </summary>
 26         /// <typeparam name="T">搜索的對象類型</typeparam>
 27         /// <param name="group">條件組</param>
 28         /// <param name="parameter">參數表達式</param>
 29         /// <param name="propertyMap">屬性映射</param>
 30         /// <returns>返回bool的條件組的表達式</returns>
 31         private static Expression BuildGroupExpression<T>(JqGridSearchRuleGroup group, ParameterExpression parameter, IDictionary<string, string> propertyMap)
 32         {
 33             List<Expression> expressions = new List<Expression>();
 34             foreach (var rule in group.Rules ?? new JqGridSearchRule[0])
 35             {
 36                 expressions.Add(BuildRuleExpression<T>(rule, parameter, propertyMap));
 37             }
 38 
 39             foreach (var subGroup in group.Groups ?? new JqGridSearchRuleGroup[0])
 40             {
 41                 expressions.Add(BuildGroupExpression<T>(subGroup, parameter, propertyMap));
 42             }
 43 
 44             if (expressions.Count == 0)
 45             {
 46                 throw new InvalidOperationException("構造where子句異常,生成了0個比較條件表達式。");
 47             }
 48 
 49             if (expressions.Count == 1)
 50             {
 51                 return expressions[0];
 52             }
 53 
 54             var expression = expressions[0];
 55             switch (group.GroupOp)
 56             {
 57                 case "AND":
 58                     foreach (var exp in expressions.Skip(1))
 59                     {
 60                         expression = Expression.AndAlso(expression, exp);
 61                     }
 62                     break;
 63                 case "OR":
 64                     foreach (var exp in expressions.Skip(1))
 65                     {
 66                         expression = Expression.OrElse(expression, exp);
 67                     }
 68                     break;
 69                 default:
 70                     throw new InvalidOperationException($"不支持創建{group.GroupOp}類型的邏輯運算表達式");
 71             }
 72 
 73             return expression;
 74         }
 75 
 76         private static readonly string[] SpecialRuleOps = {"in", "ni", "nu", "nn"};
 77 
 78         /// <summary>
 79         /// 構造條件表達式
 80         /// </summary>
 81         /// <typeparam name="T">搜索的對象類型</typeparam>
 82         /// <param name="rule">條件</param>
 83         /// <param name="parameter">參數</param>
 84         /// <param name="propertyMap">屬性映射</param>
 85         /// <returns>返回bool的條件表達式</returns>
 86         private static Expression BuildRuleExpression<T>(JqGridSearchRule rule, ParameterExpression parameter,
 87             IDictionary<string, string> propertyMap)
 88         {
 89             Expression l;
 90 
 91             string[] names = null;
 92             //如果實體屬性名稱和前端名稱不一致,或者屬性是一個自定義類型,需要繼續訪問其內部屬性,使用點號分隔
 93             if (propertyMap?.ContainsKey(rule.Field) == true)
 94             {
 95                 names = propertyMap[rule.Field].Split('.', StringSplitOptions.RemoveEmptyEntries);
 96                 l = Expression.Property(parameter, names[0]);
 97                 foreach (var name in names.Skip(1))
 98                 {
 99                     l = Expression.Property(l, name);
100                 }
101             }
102             else
103             {
104                 l = Expression.Property(parameter, rule.PascalField);
105             }
106 
107             Expression r = null; //值表達式
108             Expression e; //返回bool的各種比較表達式
109 
110             //屬於和不屬於比較是多值比較,需要調用Contains方法,而不是調用比較操作符
111             //爲空和不爲空的右值爲常量null,不需要構造
112             var specialRuleOps = SpecialRuleOps;
113 
114             var isNullable = false;
115             var pt = typeof(T);
116             if(names != null)
117             {
118                 foreach(var name in names)
119                 {
120                     pt = pt.GetProperty(name).PropertyType;
121                 }
122             }
123             else
124             {
125                 pt = pt.GetProperty(rule.PascalField).PropertyType;
126             }
127 
128             //如果屬性類型是可空值類型,取出內部類型
129             if (pt.IsDerivedFrom(typeof(Nullable<>)))
130             {
131                 isNullable = true;
132                 pt = pt.GenericTypeArguments[0];
133             }
134 
135             //根據屬性類型創建要比較的常量值表達式(也就是r)
136             if (!specialRuleOps.Contains(rule.Op))
137             {
138                 switch (pt)
139                 {
140                     case Type ct when ct == typeof(bool):
141                         r = BuildConstantExpression(rule, bool.Parse);
142                         break;
143 
144                     #region 文字
145 
146                     case Type ct when ct == typeof(char):
147                         r = BuildConstantExpression(rule, str => str[0]);
148                         break;
149                     case Type ct when ct == typeof(string):
150                         r = BuildConstantExpression(rule, str => str);
151                         break;
152 
153                     #endregion
154 
155                     #region 有符號整數
156 
157                     case Type ct when ct == typeof(sbyte):
158                         r = BuildConstantExpression(rule, sbyte.Parse);
159                         break;
160                     case Type ct when ct == typeof(short):
161                         r = BuildConstantExpression(rule, short.Parse);
162                         break;
163                     case Type ct when ct == typeof(int):
164                         r = BuildConstantExpression(rule, int.Parse);
165                         break;
166                     case Type ct when ct == typeof(long):
167                         r = BuildConstantExpression(rule, long.Parse);
168                         break;
169 
170                     #endregion
171 
172                     #region 無符號整數
173 
174                     case Type ct when ct == typeof(byte):
175                         r = BuildConstantExpression(rule, byte.Parse);
176                         break;
177                     case Type ct when ct == typeof(ushort):
178                         r = BuildConstantExpression(rule, ushort.Parse);
179                         break;
180                     case Type ct when ct == typeof(uint):
181                         r = BuildConstantExpression(rule, uint.Parse);
182                         break;
183                     case Type ct when ct == typeof(ulong):
184                         r = BuildConstantExpression(rule, ulong.Parse);
185                         break;
186 
187                     #endregion
188 
189                     #region 小數
190 
191                     case Type ct when ct == typeof(float):
192                         r = BuildConstantExpression(rule, float.Parse);
193                         break;
194                     case Type ct when ct == typeof(double):
195                         r = BuildConstantExpression(rule, double.Parse);
196                         break;
197                     case Type ct when ct == typeof(decimal):
198                         r = BuildConstantExpression(rule, decimal.Parse);
199                         break;
200 
201                     #endregion
202 
203                     #region 其它常用類型
204 
205                     case Type ct when ct == typeof(DateTime):
206                         r = BuildConstantExpression(rule, DateTime.Parse);
207                         break;
208                     case Type ct when ct == typeof(DateTimeOffset):
209                         r = BuildConstantExpression(rule, DateTimeOffset.Parse);
210                         break;
211                     case Type ct when ct == typeof(Guid):
212                         r = BuildConstantExpression(rule, Guid.Parse);
213                         break;
214                     case Type ct when ct.IsEnum:
215                         r = Expression.Constant(rule.Data.ToEnumObject(ct));
216                         break;
217 
218                     #endregion
219 
220                     default:
221                         throw new InvalidOperationException($"不支持創建{pt.FullName}類型的數據表達式");
222                 }
223             }
224 
225             if (r != null && pt.IsValueType && isNullable)
226             {
227                 var gt = typeof(Nullable<>).MakeGenericType(pt);
228                 r = Expression.Convert(r, gt);
229             }
230 
231             switch (rule.Op)
232             {
233                 case "eq": //等於
234                     e = Expression.Equal(l, r);
235                     break;
236                 case "ne": //不等於
237                     e = Expression.NotEqual(l, r);
238                     break;
239                 case "lt": //小於
240                     e = Expression.LessThan(l, r);
241                     break;
242                 case "le": //小於等於
243                     e = Expression.LessThanOrEqual(l, r);
244                     break;
245                 case "gt": //大於
246                     e = Expression.GreaterThan(l, r);
247                     break;
248                 case "ge": //大於等於
249                     e = Expression.GreaterThanOrEqual(l, r);
250                     break;
251                 case "bw": //開頭是(字符串)
252                     if (pt == typeof(string))
253                     {
254                         e = Expression.Call(l, pt.GetMethod(nameof(string.StartsWith), new[] {typeof(string)}), r);
255                     }
256                     else
257                     {
258                         throw new InvalidOperationException($"不支持創建{pt.FullName}類型的開始於表達式");
259                     }
260 
261                     break;
262                 case "bn": //開頭不是(字符串)
263                     if (pt == typeof(string))
264                     {
265                         e = Expression.Not(Expression.Call(l, pt.GetMethod(nameof(string.StartsWith), new[] {typeof(string)}), r));
266                     }
267                     else
268                     {
269                         throw new InvalidOperationException($"不支持創建{pt.FullName}類型的不開始於表達式");
270                     }
271 
272                     break;
273                 case "ew": //結尾是(字符串)
274                     if (pt == typeof(string))
275                     {
276                         e = Expression.Call(l, pt.GetMethod(nameof(string.EndsWith), new[] {typeof(string)}), r);
277                     }
278                     else
279                     {
280                         throw new InvalidOperationException($"不支持創建{pt.FullName}類型的結束於表達式");
281                     }
282 
283                     break;
284                 case "en": //結尾不是(字符串)
285                     if (pt == typeof(string))
286                     {
287                         e = Expression.Not(Expression.Call(l, pt.GetMethod(nameof(string.EndsWith), new[] {typeof(string)}), r));
288                     }
289                     else
290                     {
291                         throw new InvalidOperationException($"不支持創建{pt.FullName}類型的不結束於表達式");
292                     }
293 
294                     break;
295                 case "cn": //包含(字符串)
296                     if (pt == typeof(string))
297                     {
298                         e = Expression.Call(l, pt.GetMethod(nameof(string.Contains), new[] {typeof(string)}), r);
299                     }
300                     else
301                     {
302                         throw new InvalidOperationException($"不支持創建{pt.FullName}類型的包含表達式");
303                     }
304 
305                     break;
306                 case "nc": //不包含(字符串)
307                     if (pt == typeof(string))
308                     {
309                         e = Expression.Not(Expression.Call(l, pt.GetMethod(nameof(string.Contains), new[] {typeof(string)}), r));
310                     }
311                     else
312                     {
313                         throw new InvalidOperationException($"不支持創建{pt.FullName}類型的包含表達式");
314                     }
315 
316                     break;
317                 case "in": //屬於(是候選值列表之一)
318                     e = BuildContainsExpression(rule, l, pt);
319                     break;
320                 case "ni": //不屬於(不是候選值列表之一)
321                     e = Expression.Not(BuildContainsExpression(rule, l, pt));
322                     break;
323                 case "nu": //爲空
324                     r = Expression.Constant(null);
325                     e = Expression.Equal(l, r);
326                     break;
327                 case "nn": //不爲空
328                     r = Expression.Constant(null);
329                     e = Expression.Not(Expression.Equal(l, r));
330                     break;
331                 case "bt": //區間
332                     throw new NotImplementedException($"尚未實現創建{rule.Op}類型的比較表達式");
333                 default:
334                     throw new InvalidOperationException($"不支持創建{rule.Op}類型的比較表達式");
335             }
336 
337             return e;
338 
339             static Expression BuildConstantExpression<TValue>(JqGridSearchRule jRule, Func<string, TValue> valueConvertor)
340             {
341                 var rv = valueConvertor(jRule.Data);
342                 return Expression.Constant(rv);
343             }
344         }
345 
346         /// <summary>
347         /// 構造Contains調用表達式
348         /// </summary>
349         /// <param name="rule">條件</param>
350         /// <param name="parameter">參數</param>
351         /// <param name="parameterType">參數類型</param>
352         /// <returns>Contains調用表達式</returns>
353         private static Expression BuildContainsExpression(JqGridSearchRule rule, Expression parameter, Type parameterType)
354         {
355             Expression e = null;
356 
357             var genMethod = typeof(Queryable).GetMethods()
358                 .Single(m => m.Name == nameof(Queryable.Contains) && m.GetParameters().Length == 2);
359 
360             var jsonArray = JsonSerializer.Deserialize<string[]>(rule.Data);
361 
362             switch (parameterType)
363             {
364                 #region 文字
365 
366                 case Type ct when ct == typeof(char):
367                     if (jsonArray.Any(o => o.Length != 1)) {throw new InvalidOperationException("字符型的候選列表中存在錯誤的候選項");}
368                     e = CallContains(parameter, jsonArray, str => str[0], genMethod, ct);
369                     break;
370                 case Type ct when ct == typeof(string):
371                     e = CallContains(parameter, jsonArray, str => str, genMethod, ct);
372                     break;
373 
374                 #endregion
375 
376                 #region 有符號整數
377 
378                 case Type ct when ct == typeof(sbyte):
379                     e = CallContains(parameter, jsonArray, sbyte.Parse, genMethod, ct);
380                     break;
381                 case Type ct when ct == typeof(short):
382                     e = CallContains(parameter, jsonArray, short.Parse, genMethod, ct);
383                     break;
384                 case Type ct when ct == typeof(int):
385                     e = CallContains(parameter, jsonArray, int.Parse, genMethod, ct);
386                     break;
387                 case Type ct when ct == typeof(long):
388                     e = CallContains(parameter, jsonArray, long.Parse, genMethod, ct);
389                     break;
390 
391                 #endregion
392 
393                 #region 無符號整數
394 
395                 case Type ct when ct == typeof(byte):
396                     e = CallContains(parameter, jsonArray, byte.Parse, genMethod, ct);
397                     break;
398                 case Type ct when ct == typeof(ushort):
399                     e = CallContains(parameter, jsonArray, ushort.Parse, genMethod, ct);
400                     break;
401                 case Type ct when ct == typeof(uint):
402                     e = CallContains(parameter, jsonArray, uint.Parse, genMethod, ct);
403                     break;
404                 case Type ct when ct == typeof(ulong):
405                     e = CallContains(parameter, jsonArray, ulong.Parse, genMethod, ct);
406                     break;
407 
408                 #endregion
409 
410                 #region 小數
411 
412                 case Type ct when ct == typeof(float):
413                     e = CallContains(parameter, jsonArray, float.Parse, genMethod, ct);
414                     break;
415                 case Type ct when ct == typeof(double):
416                     e = CallContains(parameter, jsonArray, double.Parse, genMethod, ct);
417                     break;
418                 case Type ct when ct == typeof(decimal):
419                     e = CallContains(parameter, jsonArray, decimal.Parse, genMethod, ct);
420                     break;
421 
422                 #endregion
423 
424                 #region 其它常用類型
425 
426                 case Type ct when ct == typeof(DateTime):
427                     e = CallContains(parameter, jsonArray, DateTime.Parse, genMethod, ct);
428                     break;
429                 case Type ct when ct == typeof(DateTimeOffset):
430                     e = CallContains(parameter, jsonArray, DateTimeOffset.Parse, genMethod, ct);
431                     break;
432                 case Type ct when ct == typeof(Guid):
433                     e = CallContains(parameter, jsonArray, Guid.Parse, genMethod, ct);
434                     break;
435                 case Type ct when ct.IsEnum:
436                     e = CallContains(Expression.Convert(parameter, typeof(object)), jsonArray, enumString => enumString.ToEnumObject(ct), genMethod, ct);
437                     break;
438 
439                     #endregion
440             }
441 
442             return e;
443 
444             static MethodCallExpression CallContains<T>(Expression pa, string[] jArray, Func<string, T> selector, MethodInfo genericMethod, Type type)
445             {
446                 var data = jArray.Select(selector).ToArray().AsQueryable();
447                 var method = genericMethod.MakeGenericMethod(type);
448 
449                 return Expression.Call(null, method, new[] { Expression.Constant(data), pa });
450             }
451         }
452     }

使用

       此處是在 Razor Page 中使用,內部使用的其他輔助類和前端頁面代碼就不貼了,有興趣的可以在我的文章末尾找到 GitHub 項目鏈接:

 1         public async Task<IActionResult> OnGetUserListAsync([FromQuery]JqGridParameter jqGridParameter)
 2         {
 3             var usersQuery = _userManager.Users.AsNoTracking();
 4             if (jqGridParameter._search == "true")
 5             {
 6                 usersQuery = usersQuery.Where(BuildWhere<ApplicationUser>(jqGridParameter.FilterObject, null));
 7             }
 8 
 9             var users = usersQuery.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).OrderBy(u => u.InsertOrder)
10                 .Skip((jqGridParameter.Page - 1) * jqGridParameter.Rows).Take(jqGridParameter.Rows).ToList();
11             var userCount = usersQuery.Count();
12             var pageCount = Ceiling((double) userCount / jqGridParameter.Rows);
13             return new JsonResult(
14                 new
15                 {
16                     rows //數據集合
17                         = users.Select(u => new
18                         {
19                             u.UserName,
20                             u.Gender,
21                             u.Email,
22                             u.PhoneNumber,
23                             u.EmailConfirmed,
24                             u.PhoneNumberConfirmed,
25                             u.CreationTime,
26                             u.CreatorId,
27                             u.Active,
28                             u.LastModificationTime,
29                             u.LastModifierId,
30                             u.InsertOrder,
31                             u.ConcurrencyStamp,
32                             //以下爲JqGrid中必須的字段
33                             u.Id //記錄的唯一標識,可在插件中配置爲其它字段,但是必須能作爲記錄的唯一標識用,不能重複
34                         }),
35                     total = pageCount, //總頁數
36                     page = jqGridParameter.Page, //當前頁碼
37                     records = userCount //總記錄數
38                 }
39             );
40         }

       啓動項目後訪問 /Identity/Manage/Users/Index 可以嘗試使用。

結語

       通過這次實踐,深入瞭解了很多表達式樹的相關知識,表達式樹在編譯流程中還算是高級結構了,耐點心還是能看懂,IL 纔是真的暈,比原生彙編也好不到哪裏去。C# 確實很有意思,入門簡單,內部卻深邃無比,在小白和大神手上完全是兩種語言。Java 在 Java 8 時增加了 Stream 和 Lambda 表達式功能,一看就是在對標 Linq,不過那名字取的真是一言難盡,看代碼寫代碼感覺如鯁在喉,相當不爽。由於 Stream 體系缺少表達式樹,這種動態構造查詢表達式的功能從一開始就不可能支持。再加上 Java 沒有匿名類型,沒有對象初始化器,每次用 Stream 就難受的一批,中間過程的數據結構也要專門寫類,每個中間類還要獨佔一個文件,簡直暈死。抄都抄不及格!

       C# 引入 var 關鍵字核心是爲匿名類型服務,畢竟是編譯器自動生成的類型,寫代碼的時候根本沒有名字,不用 var 用什麼?簡化變量初始化代碼只是順帶的。結果 Java 又抄一半,還是最不打緊的一半,簡化變量初始化代碼。真不知道搞 Java 的那幫人在想些什麼。

 

       轉載請完整保留以下內容並在顯眼位置標註,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!

  本文地址:https://www.cnblogs.com/coredx/p/12423929.html

  完整源代碼:Github

  裏面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章