C#7.2——編寫安全高效的C#代碼 c# 中模擬一個模式匹配及匹配值抽取 走進 LINQ 的世界 移除Excel工做表密碼保護小工具含C#源代碼 騰訊QQ會員中心g_tk32算法【C#版】

C#7.2——編寫安全高效的C#代碼

2018-11-07 18:59 by 沉睡的木木夕, 123 閱讀, 0 評論, 收藏編輯javascript

原文地址:https://docs.microsoft.com/zh-cn/dotnet/csharp/write-safe-efficient-code?view=netcore-2.1
值類型的優點能避免堆分配。而劣勢就是每每伴隨的數據的拷貝。這就致使了在大量的值類型數據很難的最大化優化這些算法操做(由於伴隨着大量數據的拷貝)。而在C#7.2 中就提供了一種機制,它經過對值類型的引用來使代碼更加安全高效。使用這個特性可以最大化的減少內存分配和數據複製操做。php

這個新特性主要是如下幾個方面:html

  1. 聲明一個 readonly struct 來表示這個類型是不變的,能讓編譯器當它作參數輸入時,會保存它的拷貝。
  2. 使用 ref readonly 。當返回一個值類型,且大於 IntPtr.Size 時以及存儲的生命週期要大於這方法返回的值的時候。
  3. 當用 readonly struct 修飾的變量/類大小大於 IntPtr.Size ,那麼就應該做爲參數輸入來傳遞它來提升性能。
  4. 除非用 readonly 修飾符來聲明,永遠不要傳遞一個 struct 做爲一個輸入參數(in parameter),由於它可能會產生反作用,從而致使它的行爲變得模糊。
  5. 使用 ref struct 或者 readonly ref struct,例如 Span 或 ReadOnlySpan 以字節流的形式來處理內存。

這些技術你要面對權衡這值類型和引用類型這兩個方面帶來的影響。引用類型的變量會分配內存到堆內存上。值類型變量只包含值。他們兩個對於管理資源內存來講都是重要的。值類型當傳遞到一個方法或是從方法中返回時都會拷貝數據。這個行爲還包括拷貝值類型成員時,該值的值( This behavior includes copying the value of this when calling members of a value type. )。這個開銷視這個值類型對象數據的大小而定。引用類型是分配在堆內存的,每個新的對象都會從新分配內存到堆上。這兩個(值類型和引用)操做都會花費時間。java

readonly struct來申明一個不變的值類型結構

用 readonly 修飾符聲明一個結構體,編譯器會知道你的目的就是創建一個不變的結構體類型。編譯器就會根據兩個規則來執行這個設計決定:git

  1. 全部的字段必須是隻讀的 readonly。
  2. 全部的屬性必須是隻讀的 readonly,包括自動實現屬性。

以上兩條足已確保沒有readonly struct 修飾符的成員來修改結構的狀態—— struct 是不變的github

readonly public struct ReadonlyPoint3D { public ReadonlyPoint3D (double x, double y, double z) { this.X = x; this.Y = y; this.Z = z; } public double X { get; } public double Y { get; } public double Z { get; } }

儘量面對大對象結構體使用 ref readonly struct 語句

當這個值不是這個返回方法的本地值時,能夠經過引用返回值。經過引用返回的意思是說只拷貝了它的引用,而不是整個結構。下面的例子中 Origin 屬性不能使用 ref 返回,由於這個值是正在返回的本地變量:算法

public ReadonlyPoint3D Origin => new ReadonlyPoint3D(0,0,0);

然而,下面這個例子的屬性就能按引用返回,由於返回的值一個靜態成員:數據庫

private static ReadonlyPoint3D origin = new ReadonlyPoint3D(0,0,0); //注意:這裏返回是內部存儲的易變的引用 public ref ReadonlyPoint3D Origin => ref origin;

你若是不想調用者修改原始值,你能夠經過 readonly ref 來修飾返回值:編程

public ref readonly ReadonlyPoint3D Origin3 => ref origin;

返回 ref readonly 可以讓你保存大對象結構的引用以及可以保護你內部不變的成員數據。小程序

做爲調用方,調用者可以選擇 Origin 屬性是做爲一個值仍是 按引用只讀的值(ref readonly):

var originValue = Point3D.Origin; ref readonly var originReference = ref Point3D.Origin;

在上面這段代碼的第一行,把 Point3D 的原始屬性的常數值 Origin 拷貝並複製數據給originValue。第二段代碼只分配了引用。要注意,readonly 修飾符必須是聲明這個變量的一部分。由於這個引用是不容許被修改的。否則,就會引發編譯器編譯錯誤。

readonly 修飾符在申明的 originReference 是必須的。

編譯器要求調用者不能修改引用。企圖直接修改該值會引起編譯器的錯誤。然而,編譯器卻沒法知道成員方法修改告終構的狀態。爲了肯定對象沒有被修改,編譯器會建立一個副本並用它來調用成員信息的引用。任何修改都是對防護副本(defensive copy)的修改。

對大於 System.IntPtr.Size 的參數應用 in修飾符到 readonly struct

in 關鍵字補充了已經存在的 ref 和 out 關鍵字來按引用傳遞參數。in 關鍵字也是按引用傳遞參數,可是調用這個參數的方法不能修改這個值。

值類型做爲方法簽名參數傳到調用的方法中,且沒有用下面的修飾符時,是會發生拷貝操做的。每個修飾符指定這個變量是按引用傳遞的,避免了拷貝。以及每一個修飾符都表達不一樣的意圖:

  • out:這個方法設置參數的值來做爲參數。
  • ref:這個方法也能夠設置參數的值來做爲參數。
  • in:這個方法做爲參數沒法修改這個參數的值。

增長 in 修飾符按引用傳遞參數以及申明經過按引用傳值來避免數據的拷貝的意圖。說明你不打算修改這個做爲參數的對象。

對於只讀的那些大小超過 IntPtr.Size 的值類型來講,這個經驗常常能提升性能。例若有這些值類型(sbyte,byte,short,ushort,int,uint,long,ulong,char,float,double,decimal 以及 bool 和 enum),任何潛在的性能收益都是很小的。實際上,若是對於小於 IntPtr.Size 的類型使用按引用個傳遞,性能可能會降低。

下面這段 demo 展現了計算兩個點的3D空間的距離

public static double CalculateDistance ( in Point3D point1, in Point3D point2) { double xDifference = point1.X - point2.X; double yDifference = point1.Y - point2.Y; double zDifference = point1.Z - point2.Z; return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); }

這個方法有兩個參數結構體,每一個都有三個 double 字段。1個 double 8 個字節,因此每一個參數含有 24 字節。經過指定 in 修飾符,你傳遞了 4 個字節或 8 個字節的參數引用,4 仍是 8字節取決平臺結構(32位 一個引用 2 字節,64位一個引用 4字節)。這看似大小差別很小,可是當你的應用程序在高併發,高循環的狀況下調用這個函數,那麼性能上的差距就很明顯了。

in 修飾符也很好的補充了 out 和 ref 其餘方面。你不能建立僅修飾符(in,out,ref)不一樣的方法重載。這個新的特性拓展了已經存在 out 和 ref 參數原來相同的行爲。像 ref 和 out 修飾符,值類型因爲應用了 in 修飾符而沒法裝箱。

in 修飾符能應用在任何成員信息上:方法,委託,lambda表達式,本地函數,索引,操做符。

in 修飾符還有在其餘方面的特性,在參數上用 in 修飾的參數值你能使用字面量的值或者常數。不像 ref 和 out 參數,你沒必要在調用方用 in。下面這段代碼展現了兩個調用 CalculateDistance 的方法。第一個變量使用兩個按引用傳遞的局部變量。第二個包括了做爲這個方法調用的一部分建立的臨時變量。

var distance = CalculateDistance (point1,point2); var fromOrigin = CalculateDistance(point1,new Point3D());

這裏有一些方法,編譯器會強制執行 read-only 簽名的 in 參數。第一個,被調用的方法不能直接分配一個 in 參數。它不能分配到任何 in 字段,當這個值是值類型的時候。另外,你也不能經過 ref 和 out 修飾符來傳遞一個 in 參數到任何方法上。這些規則都應用在 in 修飾符的參數,前提是提供一個值類型的字段以及這個參數也是值類型的。事實上,這些規則適用於多個成員訪問,前提是全部級別的成員訪問的類型都是結構體。編譯器強制執行在參數中傳遞的 struct 類型,當它們的 struct 成員用做其餘方法的參數時,它們是隻讀變量。

使用 in 參數能避免潛在拷貝方面的性能開銷。它不會改變任何方法調用的語義。所以,你無需在調用方(call site)指定 in 修飾符。在調用站省略 in 修飾符會讓編譯器進行參數拷貝操做,有如下幾種緣由:

  • 存在隱式轉換,但不存在從參數類型到參數類型的標識轉換。
  • 參數是一個表達式,可是沒有已知的存儲變量。
  • 存在一個不一樣於已經存在或者是不存在 in 的重載。這種狀況下,經過值重載會更好匹配。

這些規則當你更新那些已有的而且已經用 read-only 引用參數的代碼很是有用。在調用方法裏面,你能夠經過值參數(value paramters)調用任意成員方法。在那些實例中,會拷貝 in 參數。由於編譯器會對 in 參數建立一個臨時的變量,你能夠用 in 指定默認參數的值。下面這段代碼指定了origins(point 0,0)做爲默認值做爲第二個參數:

private static double CalculateDistance2 ( in Point3D point1, in Point3D point2 = default) { double xDifference = point1.X - point2.X; double yDifference = point1.Y - point2.Y; double zDifference = point1.Z - point2.Z; return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); }

編譯器會經過引用傳遞只讀參數,指定 in 修飾符在調用方法的參數上,就像下面展現的代碼:

private static void DemoCalculateDistanceForExplicit (Point3D point1, Point3D point2) { var distance = CalculateDistance ( in point1, in point2); distance = CalculateDistance ( in point1, new Point3D ()); distance = CalculateDistance (point1, in Point3D.origin); }

這種行爲可以更容易的接受 in 參數,隨着時間的推移,大型代碼庫中性能會得到提升。首先就要添加 in 到方法簽名上。而後你能夠在調用端添加 in 修飾符以及新建一個 readonly struct 類型來使編譯器避免在更多未知建立防護拷貝的副本。

in 參數被設計也能使用在引用類型或數字值。然而,在這種狀況的性能收益是很小的。

不要使用易變的結構體做爲 in 參數

下面描述的技術主要解釋了怎樣經過返回引用以及傳遞的值引用避免數據拷貝。當參數類型是已經申明的 readonly struct 類型時,這些技術都能很好的工做。不然,編譯器在不少非只讀參數的場景下必須新建一個防護拷貝(defensive copies)副本。考慮下面這段代碼,他計算 3D 點到原地=點的距離:

private static double CalculateDistance ( in Point3D point1, in Point3D point2 = default) { double xDifference = point1.X - point2.X; double yDifference = point1.Y - point2.Y; double zDifference = point1.Z - point2.Z; return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); }

Point3D 是非只讀結構類型(readonly-ness struct)。在這個方法體中,有 6 個不一樣的屬性訪問調用。第一次檢查時,你可能以爲這些訪問都是安全的。在這以後,一個 get 讀取器不能修改這個對象的狀態。可是這裏沒有語義規則讓編譯器這樣作。它只是一個通用的約束。任何類型都能實現 get 讀取器來修改這個內部狀態。沒有這些語言保證,在調用任何成員以前,編譯器必須新建這個參數的拷貝副原本做爲臨時變量。這個臨時變量存儲在棧上,這個參數的值的副本在這個臨時變量中存儲,而且每一個成員訪問的值都會拷貝到棧上,做爲參數。在不少狀況下,當參數類型不是 readonly struct 時,這些拷貝都會對性能有害,以致於經過值傳遞要比經過只讀引用(readonly reference)傳遞快。

相反,若是距離計算方法使用不變結構,ReadonlyPoint3D,就不須要臨時變量:

private static double CalculateDistance3(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2 = default) { double xDifference = point1.X - point2.X; double yDifference = point1.Y - point2.Y; double zDifference = point1.Z - point2.Z; return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); }

當你用 readonly struct 修飾的成員時,編譯器會自動生成更多高效代碼:this 引用,而不是接受者的副本拷貝,in 參數老是按引用傳遞到成員方法中。當你使用 readonly struct 做爲 in 參數時,這種優化會節省內存。

你能夠查看程序的demo,在實例代碼倉庫 samples repository 中,它展現了使用 Benchmark.net 比較性能的差別。它比較了傳遞易變結構的值和引用,易變結構的按值傳遞和按引用傳遞。使用不變結構體的按引用傳遞是最快的。

使用 ref struct 類型在單個堆棧幀上處理塊和內存

一個語言相關的特性是申明值類型的能力,該值類型必須約束在單個堆棧對上。這個限制能讓編譯器作一些優化。主要推進這個特性體檢在 Span<T>以及相關的結構。你從使用這些新添加的以及更新的.NET API,如 Span<T> 類型來完成性能的提高。

你可能有相同的要求,在內存中使用 stackalloc 或者當使用來自於內存的交互操做API。你就爲這些需求能定義你本身的 ref struct 類型。

readonly ref struct 類型

聲明一個 readonly ref 結構體,它聯合了 ref struct 和 readonly struct 二者的收益。經過只讀的元素內存被限制在單個的棧中,而且只讀元素內存沒法被修改。

總結

使用值類型能最小化的內存分配:

  • 在局部變量和方法參數中值類型存儲在棧上分配
  • 對象的值類型成員作爲這個對象的一部分分配在棧上,並非一個單獨的分配操做。
  • 存儲返回的值類型是在棧上分配

不一樣於引用類型在相同場景下:

  • 存儲局部變量和方法參數的引用類型分配在堆上,。引用存在棧。
  • 存儲對象的成員變量是引用類型,它做爲這個對象的一部分在堆上分配內存。而不是單獨的分配這個引用。
  • 存儲返回的值是引用類型,堆分配內存。存儲引用的值存儲在棧上。

最小化的內存分配要權衡。當結構體內存大小超過引用大小時,就要拷貝更多的內存。一個引用類型指定 64 字節或者是 32 字節,它取決於平臺架構。

這些權衡/折中一般對性能影響很小。然而大對象結構體或大對象集合,對性能影響是遞增的。特別在循環和常常調用的地方影響特別明顯。

這些C#語言的加強是爲了關鍵算法的性能而設計的,內存分配問題成爲了主要的優化點。你會發現你無需常用這些特性在你寫的代碼中。然而,這些加強在 .NET 中接受。愈來愈多的 API 會運用到這些特性,你將看到你的應用程序性能的提高。

但願有個生活精彩的程序人生
 
 
 

c# 中模擬一個模式匹配及匹配值抽取

 

摘一段模式的說明, F#的: msdn是這麼描述它的:「模式」是用於轉換輸入數據的規則。模式將在整個 F# 語言中使用,採用多種方式將數據與一個或多個邏輯結構進行比較、將數據分解爲各個構成部分,或從數據中提取信息。

模式匹配自有其定義,同時也有不少種類,這裏針對相對複雜的【結構比較】和【數據抽取】進行處理(有時候也叫類型檢查與轉換)。

直白點說,就是「檢查下某個對象,看看是否有咱們感興趣的屬性成員,若是有就取出這些成員值供後續使用」。

一、結構比較

   考察以下對象

code 01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var  o =  new
            {
                a = 2,
                b = 3,
                d = 0,
                c =  new
                {
                    a1 = 7,
                    b1 = 2,
                    e =  new
                    {
                        name =  "aaa" ,
                        Id = 0
                    }
                }
            };

  

當咱們明確知道其具體類型時,能夠經過屬性訪問獲取相關值,

code 02

1
2
3
int  r1=o.a;
int  r2=o.c.a1;
string  r3=o.c.e.name;

可是,當 類型不明確 時,好比:

code 03

1
method1( object  obj)

在method1中,如何快速方便的獲取其相關屬性值?

首先,咱們知道問題的出現是由於「類型不明確」,那麼咱們要作的第一件是就是還原類型信息;

在還原類型信息以前,首先要把咱們想獲取的信息描述出來,以 code 02 爲例,

 一、但願o上有一個名爲a的屬性,類型int

  二、但願o上有一個名爲c的屬性,同時c上有一個名爲a1的屬性, 類型int

  三、但願o上有一個名爲c的屬性,同時c上有一個名爲e的屬性,同時e上有一個名爲name的屬性  類型string

 。。。。。。

不難發現,a、咱們要描述的類型信息沒必要要與原類型一致,僅表示出指望獲得的部分便可;

              b、要描述的類型信息中能正確表達層級關係

              c、要可以描述全部類型的屬性成員

              d、明確知道指望的類型信息

              e、最好使用語言環境中直接提供的技術手段

綜合以上,這裏使用匿名對象進行類型描述,簡單並且能同時知足以上5點。

code 04

1
2
3
4
5
6
7
8
9
10
11
12
var  typeinfo =  new
            {
                a = 3, //default(int)
                c =  new
                {
                    a1 = 1,
                    e =  new
                    {
                        name =  default ( string )
                    }
                }
            };

注意:類型描述時屬性值沒有意義,通常能夠用default(type),這裏使用值是爲了後面比對結果。

 

有了類型描述後,進行類型檢查就變的相對簡單了,咱們以類型描述信息爲基準,逐個檢查目標對象上有無對應的成員便可。

直接使用反射就能夠了。

code 05 

1
2
3
4
5
if  ( pi.Name==npi.Name&& pi.PropertyType == npi.PropertyType)
                 {
                     return  true .Result( new  GetValue(o => npi.Getter(o))); //擴展方法等見code 06
 
                 }<br><br><br>

code 06

複製代碼
  public struct Result<T>
    {
        public bool OK;
        public T Value;
        public Result(bool ok, T resultOrReason)
        {
            this.OK = ok;
            this.Value = resultOrReason;
        }
        public static implicit operator Result<T>(bool value)
        {
            return new Result<T>(value, default(T));
        }
        public static explicit operator bool(Result<T> value)
        {
            return value.OK;
        }
        
       
        public static bool operator ==(Result<T> a, Result<T> b)
        {
            return a.Equals(b);
        }
        public static bool operator !=(Result<T> a, Result<T> b)
        {
            return !a.Equals(b);
        }
        public override bool Equals(object obj)
        {

            var r = (Result<T>)obj;
            return this.OK == r.OK && object.Equals(this.Value, r.Value);

        }

        public override int GetHashCode()
        {
            return this.OK.GetHashCode() + (this.Value == null ? 0 : this.Value.GetHashCode());
        }
    }
複製代碼
委託:
//返回實例上全部篩選值 public delegate IEnumerable<object> GetAllValues(object instance); //返回實例上某個值 public delegate object GetValue(object instance);

 

複製代碼
//擴展方法 


//bool +結果 public static Result<Value> Result<Value>(this bool state, Value value) { return new Result<Value>(state, value); } //屬性取值, 反射 public static object Getter(this PropertyInfo info, object instance) { return info.GetValue(instance); } //新實例,反射 public static object New(this Type t, params object[] args) { return args.IsEmpty() ? Activator.CreateInstance(t) : Activator.CreateInstance(t, args); }
複製代碼

 

 考慮到結構會出現嵌套狀況,主要代碼下:

code 07

 

  

複製代碼
 1      public static Result<GetAllValues> MatchType(this Type pattern, Type target) {
 2             var pis = pattern.GetProperties();
 3             var tpis = target.GetProperties();
 4             if (pis.Length < tpis.Length)
 5             {
 6                 7                 var fac = new List<GetValue>();
 8                 for (int i = 0; i < pis.Length; i++)
 9                 {
10                     var pi = pis[i];
11                     var r = pi.MatchProp(tpis);
12                     if (r.OK)
13                     {
14                         fac.Add(r.Value);
15                         continue;
16                     }
17                     return false;
29                 }
30                 return true.Result(new GetAllValues(o => fac.Select(c => c(o))));
31             }
32             return false;
33         }
34           static Result<GetValue> MatchProp(this PropertyInfo pi, IEnumerable<PropertyInfo> target) {
35              
36             var npi =  target.FirstOrDefault(c => c.Name == pi.Name)??(pi.Name=="_"?target.FirstOrDefault(c=>c.PropertyType==pi.PropertyType):null);
37             if (npi != null) {
38                 if (pi.PropertyType.IsAnonymous() )
39                 {
40                     var r = pi.PropertyType.MatchType(npi.PropertyType);
41                     if (r.OK) {
42                         return true.Result(new GetValue(o => pi.PropertyType.New(r.Value(npi.Getter(o)).ToArray())));
43                     }
44                 }
45                 else if (  pi.PropertyType == npi.PropertyType)
46                 {
47                     return true.Result(new GetValue(o => npi.Getter(o)));
48 
49                 }
50             }
51             return false;
52 
53         }
複製代碼

代碼說明:

屬性使用 名稱+屬性類型進行檢查

若是類型描述中出現 匿名類型 屬性(line:38) ,進行層級檢查

屬性名稱爲'_' 時忽略屬性名,即 匹配第一個類型相等的屬性(僅指明一種檢查擴展方式: 能夠經過屬性信息進行特殊處理)

匹配成功後返回 針對目標對象的取值函數

 

二、目標值抽取

c#中沒法方便的動態定義變量,所以,結構檢查完成,返回的結果爲{true/false,取值函數} (Result<GetAllValues>)。

考慮使用方便,抽取值須要以友好的方式提供給使用者,這裏直接建立結構描述類型(匿名類型)的新實例做爲返回結果

藉助泛型

複製代碼
        public static Result<TResult> AsPattern<TPattern, TResult>(this TPattern pattern, object matchobj, Func<TPattern, TResult> then) {
            var matchType = matchobj.GetType();
            var patternType = typeof(TPattern);
            var matchResult = patternType.MatchType(matchType);
            if (matchResult.OK) {
                var patternInstance = patternType.New(matchResult.Value(matchobj).ToArray());
                return true.Result(then((TPattern)patternInstance));
            }
            return false;
        }
複製代碼

調用:

 

1  var result =typeinfo.AsPattern(o, (c) => c).Value;//result 類型爲code 04中typeinfo 的類型
2  //result.a;
3  //result.c.a1;
4  //result.c.e.name;

 

三、多個模式匹配及方法匹配:

   單個模式處理完成後, 多個模式處理 就是簡單的集合化。

   方法匹配:若是須要在c#中也能夠很方便的進行(無ref out 方法),慎用。

    一、使用匿名委託描述方法:new {test=default(func<string,object>)} =》指望一個名稱爲test,參數string,返回object的方法

    二、首先檢查屬性:在目標中檢查有無 名稱爲 test,類型爲func<string,object> 的屬性,如不存在,則在目標方法中查找

         關鍵代碼 

        方法簽名判斷

複製代碼
public static bool SignatureEqual(this MethodInfo mi, Type retType, IEnumerable<Type> paramTypes) {
            return mi.ReturnType == retType && paramTypes.SequenceEqual(mi.GetParameters().Select(p => p.ParameterType));
        }
//方法與委託類型的參數和返回值是否一致 public static bool SignatureEqual(this MethodInfo mi, Type delegateType) { var cmi = delegateType.GetMethod("Invoke"); return mi.SignatureEqual(cmi); } public static bool SignatureEqual(this MethodInfo mi, MethodInfo nmi) { return mi.SignatureEqual(nmi.ReturnType, nmi.GetParameters().Select(p => p.ParameterType)); }
複製代碼

    簽名一致後,返回方法調用

new GetValue(o => m.CreateDelegate(pi.PropertyType, o))//m MethodInfo

匹配完成後 直接經過 result.test("aaa")便可調用

 

 

        

    

 
 
    

  

 

 

 

 

 

 

走進 LINQ 的世界

  在此以前曾發表過三篇關於 LINQ 的隨筆:

    進階:《LINQ 標準查詢操做概述》(強烈推薦)

    技巧:《Linq To Objects - 如何操做字符串》 和 《Linq To Objects - 如何操做文件目錄

  如今,本身打算再整理一篇關於 LINQ 入門的隨筆,也是圖文並茂的哦。

 

目錄

 

LINQ 簡介

  語言集成查詢 (LINQ) 是 Visual Studio 2008 和 .NET Framework 3.5 版中引入的一項創新功能。

  傳統上,針對數據的查詢都是以簡單的字符串表示,而沒有編譯時類型檢查或 IntelliSense 支持。此外,您還必須針對如下各類數據源學習一種不一樣的查詢語言:SQL 數據庫、XML 文檔、各類 Web 服務等等。 經過LINQ, 您可使用語言關鍵字和熟悉的運算符針對強類型化對象集合編寫查詢。

  

  在 Visual Studio 中,能夠爲如下數據源編寫 LINQ 查詢:SQL Server 數據庫、XML 文檔、ADO.NET 數據集,以及支持  IEnumerable 或泛型  IEnumerable<T> 接口的任意對象集合。
  使用要求:項目 ≥ .NET Framework 3.5 。
 

1、介紹 LINQ 查詢

  查詢是一種從數據源檢索數據的表達式。隨着時間的推移,人們已經爲各類數據源開發了不一樣的語言;例如,用於關係數據庫的 SQL 和用於 XML 的 XQuery。所以,開發人員不得不針對他們必須支持的每種數據源或數據格式而學習新的查詢語言。LINQ 經過提供一種跨數據源和數據格式使用數據的一致模型,簡化了這一狀況。在 LINQ 查詢中,始終會用到對象。可使用相同的編碼模式來查詢和轉換 XML 文檔、SQL 數據庫、ADO.NET 數據集、.NET 集合中的數據以及對其有 LINQ 提供程序可用的任何其餘格式的數據。  

 

  1.1 查詢操做的三個部分

  操做三部曲:①取數據源 ②建立查詢 ③執行查詢

複製代碼
 1 internal class Program
 2 {
 3         private static void Main(string[] args)
 4         {
 5             //1.獲取數據源
 6             var nums = new int[7] { 0, 1, 2, 3, 4, 5, 6 };
 7 
 8             //2.建立查詢
 9             var numQuery =
10                 from num in nums
11                 where (num % 2) == 0
12                 select num;
13 
14             //3.執行查詢
15             foreach (var num in numQuery)
16             {
17                 Console.WriteLine("{0}", num);
18             }
19         }
20 }
複製代碼

 

   下圖顯示了完整的查詢操做。在 LINQ 中,查詢的執行與查詢自己大相徑庭;換句話說,查詢自己指的是隻建立查詢變量,不檢索任何數據。

  

  1.2 數據源

  在上一個示例中,因爲數據源是數組,所以它隱式支持泛型 IEnumerable<T> 接口。支持 IEnumerable<T> 或派生接口(如泛型 IQueryable<T>)的類型稱爲可查詢類型。  

  可查詢類型不須要進行修改或特殊處理就能夠用做 LINQ 數據源。若是源數據尚未做爲可查詢類型出如今內存中,則 LINQ 提供程序必須以此方式表示源數據。例如,LINQ to XML 將 XML 文檔加載到可查詢的  XElement 類型中:
  //從 XML 中建立數據源
  //using System.Xml.Linq;
  var contacts = XElement.Load(@"c:\xxx.xml");

  

  在 LINQ to SQL 中,首先須要建立對象關係映射。 針對這些對象編寫查詢,而後由 LINQ to SQL 在運行時處理與數據庫的通訊。

複製代碼
1     var  db = new Northwnd(@"c:\northwnd.mdf");
2     
3     //查詢在倫敦的客戶
4     var custQuery =
5         from cust in db.Customers
6         where cust.City == "London"
7         select cust;
複製代碼

 

  1.3 查詢

  查詢指定要從數據源中檢索的信息。 查詢還能夠指定在返回這些信息以前如何對其進行排序、分組和結構化。 查詢存儲在查詢變量中,並用查詢表達式進行初始化。

  以前的示例中的查詢是從整數數組中返回全部的偶數。 該查詢表達式包含三個子句: fromwhere 和  select。(若是您熟悉 SQL,您會注意到這些子句的順序與 SQL 中的順序相反。) from 子句指定數據源, where 子句指定應用篩選器, select 子句指定返回的元素的類型。 目前須要注意的是,在 LINQ 中,查詢變量自己不執行任何操做而且不返回任何數據。 它只是存儲在之後某個時刻執行查詢時爲生成結果而必需的信息。
 

  1.4 查詢執行

  1.延遲執行

    如前所述,查詢變量自己只是存儲查詢命令。  實際的查詢執行會延遲到在 foreach 語句中循環訪問查詢變量時發生。 此概念稱爲「延遲執行」。

  2.強制當即執行

    對一系列源元素執行聚合函數的查詢必須首先循環訪問這些元素。CountMaxAverage 和 First 就屬於此類查詢。因爲查詢自己必須使用 foreach 以便返回結果,所以這些查詢在執行時不使用顯式 foreach 語句。另外還要注意,這些類型的查詢返回單個值,而不是 IEnumerable 集合。 

複製代碼
1     var numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };
2 
3     var evenNumQuery =
4         from num in numbers
5         where (num % 2) == 0
6         select num;
7 
8     var evenNumCount = evenNumQuery.Count();
複製代碼

 

  若要強制當即執行任意查詢並緩存其結果,能夠調用 ToList<TSource> 或 ToArray<TSource> 方法。

複製代碼
1     var numQuery2 =
2            (from num in numbers
3             where (num % 2) == 0
4             select num).ToList();
5 
6     var numQuery3 =
7           (from num in numbers
8            where (num % 2) == 0
9             select num).ToArray();
複製代碼

 

  此外,還能夠經過在緊跟查詢表達式以後的位置放置一個 foreach 循環來強制執行查詢。可是,經過調用 ToList 或 ToArray,也能夠將全部數據緩存在單個集合對象中。 

 

2、基本 LINQ 查詢操做

  2.1 獲取數據源:from

  在 LINQ 查詢中,第一步是指定數據源。像在大多數編程語言中同樣,必須先聲明變量,才能使用它。在 LINQ 查詢中,最早使用 from 子句的目的是引入數據源和範圍變量。

1     //queryAllCustomers 是 IEnumerable<Cutsomer> 類型
2     //數據源 (customers) 和範圍變量 (cust)
3     var queryAllCustomers = from cust in customers
4                                            select cust;

  範圍變量相似於 foreach 循環中的迭代變量,但在查詢表達式中,實際上不發生迭代。執行查詢時,範圍變量將用做對 customers 中的每一個後續元素的引用。由於編譯器能夠推斷 cust 的類型,因此您沒必要顯式指定此類型。

 

  2.2 篩選:where

  也許最經常使用的查詢操做是應用布爾表達式形式的篩選器。此篩選器使查詢只返回那些表達式結果爲 true 的元素。使用 where 子句生成結果。實際上,篩選器指定從源序列中排除哪些元素。

1     var queryLondonCustomers = from cust in customers
2                                   where cust.City = "London"
3                                     select cust;

  您可使用熟悉的 C# 邏輯 AND(&&)和 OR(||) 運算符來根據須要在 where 子句中應用任意數量的篩選表達式。 

where cust.City = "London" && cust.Name = "Devon"
where cust.City = "London" || cust.Name = "Paris"

 

  2.3 排序:orderby

  一般能夠很方便地將返回的數據進行排序。orderby 子句將使返回的序列中的元素按照被排序的類型的默認比較器進行排序。

1     var queryLondonCustomers = from cust in customers
2                                where cust.City = "London"
3                                orderby cust.Name descending 
4                                select cust;

  由於 Name 是一個字符串,因此默認比較器執行從 A 到 Z 的字母排序。若要按相反順序(從 Z 到 A)對結果進行排序,請使用 orderby…descending 子句。

 

  2.4 分組:group

  使用 group 子句,您能夠按指定的鍵分組結果。

複製代碼
 1     var queryLondonCustomers = from cust in customers
 2                     group cust by cust.City;
 3 
 4     foreach (var queryLondonCustomer in queryLondonCustomers)
 5     {
 6        Console.WriteLine(queryLondonCustomer.Key);
 7        foreach (var cust in queryLondonCustomer)
 8        {
 9           Console.WriteLine(cust.Name);
10        }
11     }
複製代碼

  在本例中,cust.City 是鍵。

  在使用 group 子句結束查詢時,結果採用列表的列表形式。列表中的每一個元素是一個具備 Key 成員及根據該鍵分組的元素列表的對象。在循環訪問生成組序列的查詢時,您必須使用嵌套的 foreach 循環。外部循環用於循環訪問每一個組,內部循環用於循環訪問每一個組的成員。  

  若是您必須引用組操做的結果,可使用 into 關鍵字來建立可進一步查詢的標識符。

複製代碼
1     //custQuery 是 IEnumable<IGrouping<string, Customer>> 類型
2     var custQuery = from cust in customers
3                     group cust by cust.City
4                     into custGroup
5                     where custGroup.Count() > 2
6                     orderby custGroup.Key
7                     select custGroup;
複製代碼

 

  2.5 聯接:join

  聯接運算建立數據源中沒有顯式建模的序列之間的關聯。例如,您能夠執行聯接來查找位於同一地點的全部客戶和經銷商。在 LINQ 中,join 子句始終針對對象集合而非直接針對數據庫表運行。  

1     var innerJoinQuery = from cust in customers
2                        join dist in distributors on cust.City equals dist.City
3                        select new {CustomerName = cust.Name, DistributorName = dist.Name};

  在 LINQ 中,join 子句始終針對對象集合而非直接針對數據庫表運行。  

  在 LINQ 中,您沒必要像在 SQL 中那樣頻繁使用 join,由於 LINQ 中的外鍵在對象模型中表示爲包含項集合的屬性。

    from order in Customer.Orders...

  

  2.6 選擇(投影):select

  select 子句生成查詢結果並指定每一個返回的元素的「形狀」或類型。

  例如,您能夠指定結果包含的是整個 Customer 對象、僅一個成員、成員的子集,仍是某個基於計算或新對象建立的徹底不一樣的結果類型。當 select 子句生成除源元素副本之外的內容時,該操做稱爲「投影」。

 

3、使用 LINQ 進行數據轉換

  語言集成查詢 (LINQ) 不只可用於檢索數據,並且仍是一個功能強大的數據轉換工具。經過使用 LINQ 查詢,您能夠將源序列用做輸入,並採用多種方式修改它以建立新的輸出序列。您能夠經過排序和分組來修改該序列,而沒必要修改元素自己。可是,LINQ 查詢的最強大的功能是可以建立新類型。這一功能在 select 子句中實現。 例如,能夠執行下列任務:  

  

  3.1 將多個輸入聯接到一個輸出序列

複製代碼
 1     class Student
 2     {
 3         public string Name { get; set; }
 4 
 5         public int Age { get; set; }
 6 
 7         public string City { get; set; }
 8 
 9         public List<int> Scores { get; set; }
10     }
11 
12     class Teacher
13     {
14         public int Id { get; set; }
15 
16         public string Name { get; set; }
17 
18         public int Age { get; set; }
19 
20         public string City { get; set; }
21 
22     }
複製代碼
複製代碼
 1     internal class Program
 2     {
 3         private static void Main(string[] args)
 4         {
 5             //建立第一個數據源
 6             var students = new List<Student>()
 7             {
 8                 new Student()
 9                 {
10                     Age = 23,
11                     City = "廣州",
12                     Name = "小C",
13                     Scores = new List<int>(){85,88,83,97}
14                 },
15                 new Student()
16                 {
17                     Age = 18,
18                     City = "廣西",
19                     Name = "小明",
20                     Scores = new List<int>(){86,78,85,90}
21                 },
22                 new Student()
23                 {
24                     Age = 33,
25                     City = "夢裏",
26                     Name = "小叄",
27                     Scores = new List<int>(){86,68,73,97}
28                 }
29             };
30 
31             //建立第二個數據源
32             var teachers = new List<Teacher>()
33             {
34                 new Teacher()
35                 {
36                     Age = 35,
37                     City = "夢裏",
38                     Name = "啵哆"
39                 },
40                 new Teacher()
41                 {
42                     Age = 28,
43                     City = "雲南",
44                     Name = "小紅"
45                 },
46                 new Teacher()
47                 {
48                     Age = 38,
49                     City = "河南",
50                     Name = "麗麗"
51                 }
52             };
53 
54             //建立查詢
55             var peopleInDreams = (from student in students
56                             where student.City == "夢裏"
57                             select student.Name)
58                             .Concat(from teacher in teachers
59                                     where teacher.City == "夢裏"
60                                     select teacher.Name);
61 
62             //執行查詢
63             foreach (var person in peopleInDreams)
64             {
65                 Console.WriteLine(person);
66             }
67 
68             Console.Read();
69         }
70     }
複製代碼
   

 

  3.2 選擇各個源元素的子集

  1. 若要只選擇源元素的一個成員,請使用點運算。

1     var query = from cust in Customers
2                     select cust.City;

  

  2. 若要建立包含源元素的多個屬性的元素,可使用具備命名對象或匿名類型的對象初始值設定項。

1     var query = from cust in Customer
2                    select new {Name = cust.Name, City = cust.City};

 

  3.3 將內存中的對象轉換爲 XML

複製代碼
 1             //建立數據源
 2             var students = new List<Student>()
 3             {
 4                 new Student()
 5                 {
 6                     Age = 18,
 7                     Name = "小A",
 8                     Scores = new List<int>() {88,85,74,66 }
 9                 },
10                 new Student()
11                 {
12                     Age = 35,
13                     Name = "小B",
14                     Scores = new List<int>() {88,85,74,66 }
15                 },
16                 new Student()
17                 {
18                     Age = 28,
19                     Name = "小啥",
20                     Scores = new List<int>() {88,85,74,66 }
21                 }
22             };
23 
24             //建立查詢
25             var studentsToXml = new XElement("Root",
26                 from student in students
27                 let x = $"{student.Scores[0]},{student.Scores[1]},{student.Scores[2]},{student.Scores[3]}"
28                 select new XElement("student",
29                 new XElement("Name", student.Name),
30                 new XElement("Age", student.Age),
31                 new XElement("Scores", x))
32             );
33 
34             //執行查詢
35             Console.WriteLine(studentsToXml);
複製代碼

 

  3.4 對源元素執行操做

  輸出序列可能不包含源序列的任何元素或元素屬性。輸出多是經過將源元素用做輸入參數計算出的值的序列。

複製代碼
 1             //數據源
 2             double[] radii = {1, 2, 3};
 3 
 4             //建立查詢
 5             var query = from radius in radii
 6                 select $"{radius * radius * 3.14}";
 7 
 8             //執行查詢
 9             foreach (var i in query)
10             {
11                 Console.WriteLine(i);
12             }
複製代碼

   【備註】$"{radius * radius * 3.14}" 至關於 string.Format("{0}",radius * radius * 3.14),這裏採用的是 C# 6.0 的語法。

 

4、LINQ 查詢操做的類型關係

  LINQ 查詢操做在數據源、查詢自己及查詢執行中是強類型的。查詢中變量的類型必須與數據源中元素的類型和 foreach 語句中迭代變量的類型兼容。強類型能夠保證在編譯時捕獲類型錯誤,以便及時改正。

 

  4.1 不轉換源數據的查詢

  下圖演示不對數據執行轉換的 LINQ to Objects 查詢操做。源包含一個字符串序列,查詢輸出也是一個字符串序列。 

  ①數據源的類型參數決定範圍變量的類型。

  ②選擇的對象的類型決定查詢變量的類型。此處的 name 爲一個字符串。所以,查詢變量是一個 IEnumerable<字符串>。  

  ③在 foreach 語句中循環訪問查詢變量。由於查詢變量是一個字符串序列,因此迭代變量也是一個字符串。  

 

  4.2 轉換源數據的查詢

  下圖演示對數據執行簡單轉換的 LINQ to SQL 查詢操做。查詢將一個 Customer 對象序列用做輸入,並只選擇結果中的 Name 屬性。由於 Name 是一個字符串,因此查詢生成一個字符串序列做爲輸出。  

  ①數據源的類型參數決定範圍變量的類型。

  ②select 語句返回 Name 屬性,而非完整的 Customer 對象。由於 Name 是一個字符串,因此 custNameQuery 的類型參數是 string,而非Customer。  

  ③由於 custNameQuery 是一個字符串序列,因此 foreach 循環的迭代變量也必須是 string

 

  下圖演示另外一種轉換。select 語句返回只捕獲原始 Customer 對象的兩個成員的匿名類型。

  ①數據源的類型參數始終爲查詢中的範圍變量的類型。

  ②由於 select 語句生成匿名類型,因此必須使用 var 隱式類型化查詢變量。

  ③由於查詢變量的類型是隱式的,因此 foreach 循環中的迭代變量也必須是隱式的。

 

  4.3 讓編譯器推斷類型信息

  您也可使用關鍵字 var,可用於查詢操做中的任何局部變量。可是,編譯器爲查詢操做中的各個變量提供強類型。  

 

5、LINQ 中的查詢語法和方法語法

  咱們編寫的 LINQ 查詢語法,在編譯代碼時,CLR 會將查詢語法轉換爲方法語法。這些方法調用標準查詢運算符的名稱相似 WhereSelectGroupByJoinMax和 Average,咱們也是能夠直接使用這些方法語法的。  

  查詢語法和方法語法語義相同,可是,許多人員發現查詢語法更簡單、更易於閱讀。某些查詢必須表示爲方法調用。例如,必須使用方法調用表示檢索元素的數量與指定的條件的查詢。還必須使用方法須要檢索元素的最大值在源序列的查詢。System.Linq 命名空間中的標準查詢運算符的參考文檔一般使用方法語法。

 

  5.1 標準查詢運算符擴展方法

複製代碼
 1         static void Main(string[] args)
 2         {
 3             var nums = new int[4] { 1, 2, 3, 4 };
 4             
 5             //建立查詢表達式
 6             var qureyNums = from n in nums
 7                             where n % 2 == 0
 8                             orderby n descending
 9                             select n;
10 
11             Console.WriteLine("qureyNums:");
12             foreach (var n in qureyNums)
13             {
14                 Console.WriteLine(n);
15             }
16     
17             //使用方法進行查詢
18             var queryNums2 = nums.Where(n => n % 2 == 0).OrderByDescending(n => n);
19 
20             Console.WriteLine("qureyNums2:");
21             foreach (var n in queryNums2)
22             {
23                 Console.WriteLine(n);
24             }
25 
26             Console.Read();
27         }
複製代碼

  兩個示例的輸出是相同的。您能夠看到兩種形式的查詢變量的類型是相同的:IEnumerable<T>。  

  若要了解基於方法的查詢,讓咱們進一步地分析它。注意,在表達式的右側,where 子句如今表示爲對 numbers 對象的實例方法,在您從新調用該對象時其類型爲 IEnumerable<int>。若是您熟悉泛型 IEnumerable<T> 接口,那麼您就會了解,它不具備 Where 方法。可是,若是您在 Visual Studio IDE 中調用 IntelliSense 完成列表,那麼您不只將看到 Where 方法,並且還會看到許多其餘方法,如 SelectSelectManyJoin 和Orderby。下面是全部標準查詢運算符。 

  儘管看起來 IEnumerable<T> 彷佛已被從新定義以包括這些附加方法,但事實上並不是如此。這些標準查詢運算符都是做爲「擴展方法」實現的。

 

  5.2 Lambda 表達式

  在前面的示例中,通知該條件表達式 (num % 2 == 0) 是做爲內聯參數。Where 方法:Where(num => num % 2 == 0) 此內聯表達式稱爲lambda 表達式。將代碼編寫爲匿名方法或泛型委託或表達式樹是一種便捷的方法,不然編寫起來就要麻煩得多。=> 是 lambda 運算符,可讀爲「goes to」。運算符左側的 num 是輸入變量,與查詢表達式中的 num 相對應。編譯器可推斷 num 的類型,由於它瞭解 numbers 是泛型 IEnumerable<T> 類型。lambda 表達式與查詢語法中的表達式或任何其餘 C# 表達式或語句中的表達式相同;它能夠包括方法調用和其餘複雜邏輯。「返回值」就是表達式結果。  

 

  5.3 查詢的組合性

  在上面的代碼示例中,請注意 OrderBy 方法是經過在對 Where 的調用中使用點運算符來調用的。Where 生成篩選序列,而後 Orderby 經過對該序列排序來對它進行操做。由於查詢會返回 IEnumerable,因此您可經過將方法調用連接在一塊兒,在方法語法中將這些查詢組合起來。這就是在您經過使用查詢語法編寫查詢時編譯器在後臺所執行的操做。而且因爲查詢變量不存儲查詢的結果,所以您能夠隨時修改它或將它用做新查詢的基礎,即便在執行它後。

 

傳送門

  入門:《走進 LINQ 的世界

  進階:《LINQ 標準查詢操做概述》(強烈推薦)

  技巧:《Linq To Objects - 如何操做字符串》 和 《Linq To Objects - 如何操做文件目錄

 

 


本文首聯:http://www.cnblogs.com/liqingwen/p/5832322.html

【參考】https://msdn.microsoft.com/zh-cn/library/bb397897(v=vs.100).aspx 等

【來源】本文引用部分微軟官方文檔的圖片

 

 

移除Excel工做表密碼保護小工具含C#源代碼

2018-11-07 19:44 by zhoujie, 61 閱讀, 0 評論, 收藏編輯

有朋友發了個Excel.xlsx文件給我,讓我幫忙看看裏面是怎麼作出來的。打開審閱後發現,每一個Excel工做表都添加了密碼保護:

看不到裏面的隱藏列和公式等等,感受很神祕。因而研究了一下Excel文件的格式,作了一個解除工做表密碼的小程序:

原理很簡單:.

xlsx文件實際上是一個zip壓縮文件,而每一個文件都是xml格式。微軟專門提供了SDK,我是直接用DotNetZip操做的,移除每一個工做表的加密節點便可。

獲取工具和源代碼

 

 

騰訊QQ會員中心g_tk32算法【C#版】

 

最近用C#寫qq活動輔助類程序,碰到了會員簽到的gtk算法不同,後來網上找了看,發現有php版的(https://www.oschina.net/code/snippet_1378052_48831)

後來參考了php版的查php相關的資料用C#寫了一個:

複製代碼
 /// <summary>
        /// 計算gtk32值
        /// </summary>
        /// <param name="skey"></param>
        /// <returns></returns>
        public static string GetGTK32(string skey)
        {
            var hash = 5381;
            var md5Key = "tencentQQVIP123443safde&!%^%1282";
            var start = hash << 5;
            var result = string.Empty;
            for (int i = 0; i < skey.Length; i++)
            {
                var ascode = CharToASCII(skey.Substring(i, 1));
                result += (hash << 5) + ascode;
                hash = ascode;
            }
            var str = start + (result + md5Key);
            return GetMD5(str);
        }

        /// <summary>
        /// MD5加密
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public static string GetMD5(string text)
        {
            StringBuilder sb = new StringBuilder();
            using (MD5 md5 = MD5.Create())
            {
                byte[] md5Byte = md5.ComputeHash(Encoding.Default.GetBytes(text));
                for (int i = 0; i < md5Byte.Length; i++)
                {
                    sb.Append(md5Byte[i].ToString("x2"));
                }
            }
            return sb.ToString();
        }

        /// <summary>
        /// /*字符轉化爲ASCII*/
        /// </summary>
        /// <param name="character"></param>
        /// <returns></returns>
        static int CharToASCII(string character)
        {
            ASCIIEncoding asciiEncoding = new ASCIIEncoding();
            int intAsciiCode = asciiEncoding.GetBytes(character)[0];
            return intAsciiCode;
        }
複製代碼

另附上解析cookie中的skey和p_skey方法和gtk算法:

複製代碼
        /// <summary>
        /// 解析cookie,取到Skey
        /// </summary>
        /// <param name="cookies">騰訊QQ cookie</param>
        /// <returns></returns>
        public static string GetSkey(string cookies)
        {
            #region 字符串分割解析
            //var keyStr = "skey=";
            //var index = cookies.IndexOf(keyStr) + keyStr.Length;
            //var skey = cookies.Remove(0, index);
            //if (skey.Contains(";") && skey.Length > 10)
            //{
            //    var laindex = cookies.IndexOf(";");
            //    skey = skey.Remove(10);
            //} 
            #endregion
            var skey = Regex.Match(cookies, "skey=(.){10}?").Value.Remove(0, 5);
            if (skey.Length > 10)
            {
                skey.Remove(10);
            }
            return skey;
        }

        /// <summary>
        /// 解析cookie,取到p_skey
        /// </summary>
        /// <param name="cookies">騰訊QQ cookie</param>
        /// <returns></returns>
        public static string Getp_skey(string cookies)
        {
            return Regex.Match(cookies, "p_skey=(.)+?_").Value.Remove(0, 7);
        }

        /// <summary>
        /// 算出g_tk
        /// </summary>
        /// <param name="sKey">cookie中的sKey值</param>
        /// <returns></returns>
        public static string GetGTK(string sKey)
        {
            var hash = 5381;
            for (int i = 0, len = sKey.Length; i < len; ++i)
            {
                hash += (hash << 5) + sKey[i];
            }
            return (hash & 0x7fffffff).ToString();
        }
複製代碼
相關文章
相關標籤/搜索