0
点赞
收藏
分享

微信扫一扫

C#玩转指针(二):预处理器、using、partial关键字与region的妙用

欲练神功,引刀自宫。为了避免内存管理的烦恼,Java咔嚓一下,把指针砍掉了。当年.Net也追随潮流,咔嚓了一下,化名小桂子,登堂入室进了皇宫。康熙往下面一抓:咦?还在?——原来是假太监韦小宝。

打开unsafe选项,C#指针就biu的一下子蹦出来了。指针很强大,没必要抛弃这一强大的工具。诚然,在大多数情况下用不上指针,但在特定的情况下还是需要用到的。比如:

(1)大规模的运算中使用指针来提高性能;

(2)与非托管代码进行交互;

(3)在实时程序中使用指针,自行管理内存和对象的生命周期,以减少GC的负担。

目前使用指针的主要语言是C和C++。但是由于语法限制,C和C++中的指针的玩法很单调,在C#中,可以进行更优雅更好玩的玩法。本文是《​​重新认识C#: 玩转指针​​​》一文的续篇,主要是对《​​重新认识C#: 玩转指针​​》内容进行总结和改进。

 

C#下使用指针有两大限制:

(1)使用指针只能操作struct,不能操作class;

(2)不能在泛型类型代码中使用未定义类型的指针。

第一个限制没办法突破,因此需要将指针操作的类型设为struct。struct + 指针,恩,就把C#当更好的C来用吧。对于第二个限制,写一个预处理器来解决问题。

 

下面是我写的简单的C#预处理器的代码,不到200行:

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

  1 using System; 

  2 using System.Collections.Generic; 

  3 using System.IO; 

  4 using System.Text; 

  5 using System.Text.RegularExpressions; 

  6 

  7 namespace Orc.Util.Csmacro 

  8 { 

  9     class Program 

 10     { 

 11         static Regex includeReg = new Regex("#region\\s+include.+\\s+#endregion"); 

 12         static Regex mixinReg = new Regex("(?<=#region\\s+mixin\\s)[\\s|\\S]+(?=#endregion)"); 

 13         /// <summary> 

 14         /// Csmacro [dir|filePath] 

 15         /// 

 16         /// 语法: 

 17         ///     #region include "" 

 18         ///     #endregion 

 19         ///     

 20         /// </summary> 

 21         /// <param name="args"></param> 

 22         static void 

 23             #region include<> 

 24                 Main 

 25             #endregion 

 26             (string[] args) 

 27         { 

 28             if (args.Length != 1) 

 29             { 

 30                 PrintHelp(); 

 31                 return; 

 32             } 

 33 

 34             String filePath = args[0]; 

 35 

 36             Path.GetDirectoryName(filePath); 

 37             String dirName = Path.GetDirectoryName(filePath); 

 38 #if DEBUG 

 39             Console.WriteLine("dir:" + dirName); 

 40 #endif 

 41             String fileName = Path.GetFileName(filePath); 

 42 #if DEBUG 

 43             Console.WriteLine("file:" + fileName); 

 44 #endif 

 45 

 46             if (String.IsNullOrEmpty(fileName)) 

 47             { 

 48                 Csmacro(new DirectoryInfo(dirName)); 

 49             } 

 50             else 

 51             { 

 52                 if (fileName.EndsWith(".cs") == false) 

 53                 { 

 54                     Console.WriteLine("Csmacro只能处理后缀为.cs的源程序."); 

 55                 } 

 56                 else 

 57                 { 

 58                     Csmacro(new FileInfo(fileName)); 

 59                 } 

 60             } 

 61 

 62             Console.WriteLine("[Csmacro]:处理完毕."); 

 63 

 64 #if DEBUG 

 65             Console.ReadKey(); 

 66 #endif 

 67         } 

 68 

 69         static void Csmacro(DirectoryInfo di) 

 70         { 

 71             Console.WriteLine("[Csmacro]:进入目录" + di.FullName); 

 72 

 73             foreach (FileInfo fi in di.GetFiles("*.cs", SearchOption.AllDirectories)) 

 74             { 

 75                 Csmacro(fi); 

 76             } 

 77         } 

 78 

 79         static void Csmacro(FileInfo fi) 

 80         { 

 81             String fullName = fi.FullName; 

 82             if (fi.Exists == false) 

 83             { 

 84                 Console.WriteLine("[Csmacro]:文件不存在-" + fullName); 

 85             } 

 86             else if (fullName.EndsWith("_Csmacro.cs")) 

 87             { 

 88                 return; 

 89             } 

 90             else 

 91             { 

 92                 String text = File.ReadAllText(fullName); 

 93 

 94                 DirectoryInfo parrentDirInfo = fi.Directory; 

 95 

 96                 MatchCollection mc = includeReg.Matches(text); 

 97                 if (mc == null || mc.Count == 0) return; 

 98                 else 

 99                 { 

100                     Console.WriteLine("[Csmacro]:处理文件" + fullName); 

101 

102                     StringBuilder sb = new StringBuilder(); 

103 

104                     Int32 from = 0; 

105                     foreach (Match item in mc) 

106                     { 

107                         sb.Append(text.Substring(from, item.Index - from)); 

108                         from = item.Index + item.Length; 

109                         sb.Append(Csmacro(parrentDirInfo, item.Value)); 

110                     } 

111 

112                     sb.Append(text.Substring(from, text.Length - from)); 

113 

114                     String newName = fullName.Substring(0, fullName.Length - 3) + "_Csmacro.cs"; 

115                     if (File.Exists(newName)) 

116                     { 

117                         Console.WriteLine("[Csmacro]:删除旧文件" + newName); 

118                     } 

119                     File.WriteAllText(newName, sb.ToString()); 

120                     Console.WriteLine("[Csmacro]:生成文件" + newName); 

121                 } 

122             } 

123         } 

124 

125         static String Csmacro(DirectoryInfo currentDirInfo, String text) 

126         { 

127             String outfilePath = text.Replace("#region", String.Empty).Replace("#endregion", String.Empty).Replace("include",String.Empty).Replace("\"",String.Empty).Trim(); 

128             try 

129             { 

130                 if (Path.IsPathRooted(outfilePath) == false) 

131                 { 

132                     outfilePath = currentDirInfo.FullName + @"\" + outfilePath; 

133                 } 

134                 FileInfo fi = new FileInfo(outfilePath); 

135                 if (fi.Exists == false) 

136                 { 

137                     Console.WriteLine("[Csmacro]:文件" + fi.FullName + "不存在."); 

138                     return text; 

139                 } 

140                 else 

141                 { 

142                     return GetMixinCode(File.ReadAllText(fi.FullName)); 

143                 } 

144             } 

145             catch (Exception ex) 

146             { 

147                 Console.WriteLine("[Csmacro]:出现错误(" + outfilePath + ")-" + ex.Message); 

148             } 

149             finally 

150             { 

151             } 

152             return text; 

153         } 

154 

155         static String GetMixinCode(String txt) 

156         { 

157             Match m = mixinReg.Match(txt); 

158             if (m.Success == true) 

159             { 

160                 return m.Value; 

161             } 

162             else return String.Empty; 

163         } 

164 

165         static void PrintHelp() 

166         { 

167             Console.WriteLine("Csmacro [dir|filePath]"); 

168         } 

169     } 

170 }


然后编译为 Csmacro.exe ,放入系统路径下。在需要使用预处理器的项目中添加 Pre-build event command lind:


Csmacro.exe $(ProjectDir)


Visual Studio 有个很好用的关键字 “region” ,我们就把它当作我们预处理器的关键字。include 一个文件的语法是:


#region include "xxx.cs"
#endregion


一个文件中可以有多个 #region include 块。

被引用的文件不能全部引用,因为一个C#文件中一般包含有 using,namespace … 等,全部引用的话会报编译错误。因此,在被引用文件中,需要通过关键字来规定被引用的内容:


#region mixin

#endregion


这个预处理器比较简单。被引用的文件中只能存在一个 #region mixin 块,且在这个region的内部,不能有其它的region块。

预处理器 Csmacro.exe 的作用就是找到所有 cs 文件中的 #region include 块,根据 #region include  路径找到被引用文件,将该文件中的 #region mixin 块 取出,替换进 #region include 块中,生成一个以_Csmacro.cs结尾的新文件 。

由于C#的两个语法糖“partial” 和 “using”,预处理器非常好用。如果没有这两个语法糖,预处理器会很丑陋不堪。(谁说语法糖没价值!一些小小的语法糖,足以实现新的编程范式。)

partial 关键字 可以保证一个类型的代码存在几个不同的源文件中,这保证了预处理器的执行,您可以像写正常的代码一样编写公共部分代码,并且正常编译。

using 关键字可以为类型指定一个的别名。这是一个不起眼的语法糖,却在本文中非常重要:它可以为不同的类型指定一个相同的类型别名。之所以引入预处理器,就是为了复用包含指针的代码。我们可以将代码抽象成两部分:变化部分和不变部分。一般来说,变化部分是类型的型别,如果还有其它非类型的变化,我们也可以将这些变化封装成新的类型。这样一来,我们可以将变化的类型放在源文件的顶端,使用using 关键字,命名为固定的别名。然后把不变部分的代码,放在 #region mixin 块中。这样的话,让我们需要 #region include 时,只需要在 #region include  块的前面(需要在namespace {} 的外部)为类型别名指定新的类型。

举例说明,位图根据像素的格式可以分为很多种,这里假设有两种图像,一种是像素是一个Byte的灰度图像ImageU8,一个是像素是一个Argb32的彩色图像ImageArgb32。ImageU8代码如下:

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 public class ImageU8

 2 {

 3     public Int32 Width { get; set; }

 4     public Int32 Height { get; set; }

 5 

 6     public unsafe Byte* Pointer;

 7     public unsafe void SetValue(Int32 row, Int32 col, Byte value)

 8     {

 9         Pointer[row * Width + col] = value;

10     }

11 }


 在 ImageArgb32 中,我们也要写重复的代码:

 

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 public class ImageArgb32

 2 {

 3     public Int32 Width { get; set; }

 4     public Int32 Height { get; set; }

 5 

 6     public unsafe Argb32* Pointer;

 7     public unsafe void SetValue(Int32 row, Int32 col, Argb32 value)

 8     {

 9         Pointer[row * Width + col] = value;

10     }

11 }


 

对于 Width和Height属性,我们可以建立基类来进行抽象和复用,然而,对于m_pointer和SetValue方法,如果放在基类中,则需要抹去类型信息,且变的十分丑陋。由于C#不支持泛型类型的指针,也无法提取为泛型代码。

使用 Csmacro.exe 预处理器,我们就可以很好的处理。

首先,建立一个模板文件 Image_Template.cs 




C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 using TPixel = System.Byte; 

 2 

 3 using System; 

 4 

 5 namespace XXX.Hidden 

 6 { 

 7     class Image_Template 

 8     { 

 9         public Int32 Width { get; set; } 

10         public Int32 Height { get; set; } 

11 

12         #region mixin 

13 

14         public unsafe TPixel* Pointer; 

15         public unsafe void SetValue(Int32 row, Int32 col, TPixel value) 

16         { 

17             Pointer[row * Width + col] = value; 

18         } 

19 

20         #endregion 

21     } 

22 }


然后建立一个基类 BaseImage,再从BaseImage派生ImageU8和ImageArgb32。两个派生类都是 partial 类:

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 using System; 

 2 using System.Collections.Generic; 

 3 using System.Text; 

 4 

 5 namespace XXX 

 6 { 

 7     public class BaseImage 

 8     { 

 9         public Int32 Width { get; set; } 

10         public Int32 Height { get; set; } 

11     } 

12 

13     public partial class ImageU8 : BaseImage 

14     { 

15     } 

16 

17     public partial class ImageArgb32 : BaseImage 

18     { 

19     } 

20 }


下面我们建立一个 ImageU8_ClassHelper.cs 文件,来 #region include 引用上面的模板文件:

 1 using TPixel = System.Byte; 

 2 

 3 using System; 

 4 namespace XXX 

 5 { 

 6     public partial class ImageU8 

 7     { 

 8         #region include "Image_Template.cs" 

 9         #endregion 

10     } 

11 }


编译,编译器会自动生成文件 “ImageU8_ClassHelper_Csmacro.cs” 。将这个文件引入项目中,编译通过。这个文件内容是:

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 using TPixel = System.Byte; 

 2 

 3 using System; 

 4 namespace XXX 

 5 { 

 6     public partial class ImageU8 

 7     {

 8 

 9         public unsafe TPixel* Pointer; 

10         public unsafe void SetValue(Int32 row, Int32 col, TPixel value) 

11         { 

12             Pointer[row * Width + col] = value; 

13         } 

14 

15     } 

16 }


对于 ImageArgb32 类也可以进行类似操作。

 

从这个例子可以看出,使用 partial 关键字,能够让原代码、模板代码、ClassHelper代码三者共存。使用 using 关键字,可以分离出代码中变化的部分出来。

 

下面是我写的图像操作的一些模板代码:

(1)通过模板提供指针和索引器:

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 using TPixel = System.Byte; 

 2 using TCache = System.Int32; 

 3 using TKernel = System.Int32; 

 4 

 5 using System; 

 6 using System.Collections.Generic; 

 7 using System.Text; 

 8 

 9 namespace Orc.SmartImage.Hidden 

10 { 

11     public abstract class Image_Template : UnmanagedImage<TPixel> 

12     { 

13         private Image_Template() 

14             : base(1,1) 

15         { 

16             throw new NotImplementedException(); 

17         } 

18 

19         #region mixin 

20 

21         public unsafe TPixel* Start { get { return (TPixel*)this.StartIntPtr; } } 

22 

23         public unsafe TPixel this[int index] 

24         { 

25             get 

26             { 

27                 return Start[index]; 

28             } 

29             set 

30             { 

31                 Start[index] = value; 

32             } 

33         } 

34 

35         public unsafe TPixel this[int row, int col] 

36         { 

37             get 

38             { 

39                 return Start[row * this.Width + col]; 

40             } 

41             set 

42             { 

43                 Start[row * this.Width + col] = value; 

44             } 

45         } 

46 

47         public unsafe TPixel* Row(Int32 row) 

48         { 

49             if (row < 0 || row >= this.Height) throw new ArgumentOutOfRangeException("row"); 

50             return Start + row * this.Width; 

51         } 

52 

53         #endregion 

54     } 

55 }


 

(2)通过模板提供常用的操作和Lambda表达式支持

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

  1 using TPixel = System.Byte; 

  2 using TCache = System.Int32; 

  3 using TKernel = System.Int32; 

  4 

  5 using System; 

  6 using System.Collections.Generic; 

  7 using System.Text; 

  8 

  9 namespace Orc.SmartImage.Hidden 

 10 { 

 11     static class ImageClassHelper_Template 

 12     { 

 13         #region mixin 

 14 

 15         public unsafe delegate void ActionOnPixel(TPixel* p); 

 16         public unsafe delegate void ActionWithPosition(Int32 row, Int32 column, TPixel* p); 

 17         public unsafe delegate Boolean PredicateOnPixel(TPixel* p); 

 18 

 19         public unsafe static void ForEach(this UnmanagedImage<TPixel> src, ActionOnPixel handler) 

 20         { 

 21             TPixel* start = (TPixel*)src.StartIntPtr; 

 22             TPixel* end = start + src.Length; 

 23             while (start != end) 

 24             { 

 25                 handler(start); 

 26                 ++start; 

 27             } 

 28         } 

 29 

 30         public unsafe static void ForEach(this UnmanagedImage<TPixel> src, ActionWithPosition handler) 

 31         { 

 32             Int32 width = src.Width; 

 33             Int32 height = src.Height; 

 34 

 35             TPixel* p = (TPixel*)src.StartIntPtr; 

 36             for (Int32 r = 0; r < height; r++) 

 37             { 

 38                 for (Int32 w = 0; w < width; w++) 

 39                 { 

 40                     handler(w, r, p); 

 41                     p++; 

 42                 } 

 43             } 

 44         } 

 45 

 46         public unsafe static void ForEach(this UnmanagedImage<TPixel> src, TPixel* start, uint length, ActionOnPixel handler) 

 47         { 

 48             TPixel* end = start + src.Length; 

 49             while (start != end) 

 50             { 

 51                 handler(start); 

 52                 ++start; 

 53             } 

 54         } 

 55 

 56         public unsafe static Int32 Count(this UnmanagedImage<TPixel> src, PredicateOnPixel handler) 

 57         { 

 58             TPixel* start = (TPixel*)src.StartIntPtr; 

 59             TPixel* end = start + src.Length; 

 60             Int32 count = 0; 

 61             while (start != end) 

 62             { 

 63                 if (handler(start) == true) count++; 

 64                 ++start; 

 65             } 

 66             return count; 

 67         } 

 68 

 69         public unsafe static Int32 Count(this UnmanagedImage<TPixel> src, Predicate<TPixel> handler) 

 70         { 

 71             TPixel* start = (TPixel*)src.StartIntPtr; 

 72             TPixel* end = start + src.Length; 

 73             Int32 count = 0; 

 74             while (start != end) 

 75             { 

 76                 if (handler(*start) == true) count++; 

 77                 ++start; 

 78             } 

 79             return count; 

 80         } 

 81 

 82         public unsafe static List<TPixel> Where(this UnmanagedImage<TPixel> src, PredicateOnPixel handler) 

 83         { 

 84             List<TPixel> list = new List<TPixel>(); 

 85 

 86             TPixel* start = (TPixel*)src.StartIntPtr; 

 87             TPixel* end = start + src.Length; 

 88             while (start != end) 

 89             { 

 90                 if (handler(start) == true) list.Add(*start); 

 91                 ++start; 

 92             } 

 93 

 94             return list; 

 95         } 

 96 

 97         public unsafe static List<TPixel> Where(this UnmanagedImage<TPixel> src, Predicate<TPixel> handler) 

 98         { 

 99             List<TPixel> list = new List<TPixel>(); 

100 

101             TPixel* start = (TPixel*)src.StartIntPtr; 

102             TPixel* end = start + src.Length; 

103             while (start != end) 

104             { 

105                 if (handler(*start) == true) list.Add(*start); 

106                 ++start; 

107             } 

108 

109             return list; 

110         } 

111 

112         /// <summary> 

113         /// 查找模板。模板中值代表实际像素值。负数代表任何像素。返回查找得到的像素的左上端点的位置。 

114         /// </summary> 

115         /// <param name="template"></param> 

116         /// <returns></returns> 

117         public static unsafe List<System.Drawing.Point> FindTemplate(this UnmanagedImage<TPixel> src, int[,] template) 

118         { 

119             List<System.Drawing.Point> finds = new List<System.Drawing.Point>(); 

120             int tHeight = template.GetUpperBound(0) + 1; 

121             int tWidth = template.GetUpperBound(1) + 1; 

122             int toWidth = src.Width - tWidth + 1; 

123             int toHeight = src.Height - tHeight + 1; 

124             int stride = src.Width; 

125             TPixel* start = (TPixel*)src.SizeOfType; 

126             for (int r = 0; r < toHeight; r++) 

127             { 

128                 for (int c = 0; c < toWidth; c++) 

129                 { 

130                     TPixel* srcStart = start + r * stride + c; 

131                     for (int rr = 0; rr < tHeight; rr++) 

132                     { 

133                         for (int cc = 0; cc < tWidth; cc++) 

134                         { 

135                             int pattern = template[rr, cc]; 

136                             if (pattern >= 0 && srcStart[rr * stride + cc] != pattern) 

137                             { 

138                                 goto Next; 

139                             } 

140                         } 

141                     } 

142 

143                     finds.Add(new System.Drawing.Point(c, r)); 

144 

145                 Next: 

146                     continue; 

147                 } 

148             } 

149 

150             return finds; 

151         } 

152 

153         #endregion 

154     } 

155 }


配合lambda表达式,用起来很爽。在方法“FindTemplate”中,有这一句:


if (pattern >= 0 && srcStart[rr * stride + cc] != pattern)


其中 srcStart[rr * stride + cc] 是 TPixel 不定类型,而 pattern 是 int 类型,两者之间需要进行比较,但是并不是所有的类型都提供和整数之间的 != 操作符。为此,我建立了个新的模板 TPixel_Template。

 

(3)通过模板提供 != 操作符 的定义

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 using TPixel = System.Byte; 

 2 using System; 

 3 

 4 namespace Orc.SmartImage.Hidden 

 5 { 

 6     public struct TPixel_Template 

 7     { 

 8         /* 

 9         #region mixin 

10 

11         public static Boolean operator ==(TPixel lhs, int rhs) 

12         { 

13             throw new NotImplementedException(); 

14         } 

15 

16         public static Boolean operator !=(TPixel lhs, int rhs) 

17         { 

18             throw new NotImplementedException(); 

19         } 

20 

21         public static Boolean operator ==(TPixel lhs, double rhs) 

22         { 

23             throw new NotImplementedException(); 

24         } 

25 

26         public static Boolean operator !=(TPixel lhs, double rhs) 

27         { 

28             throw new NotImplementedException(); 

29         } 

30 

31         public static Boolean operator ==(TPixel lhs, float rhs) 

32         { 

33             throw new NotImplementedException(); 

34         } 

35 

36         public static Boolean operator !=(TPixel lhs, float rhs) 

37         { 

38             throw new NotImplementedException(); 

39         } 

40 

41         #endregion 

42 

43         */ 

44     } 

45 }


这里,在 #region mixin  块被注释掉了,不注释掉编译器会报错。注释之后,不会影响程序预处理。

 

通过 ClassHelper类来使用模板:

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 1 using System; 

 2 using System.Collections.Generic; 

 3 using System.Text; 

 4 

 5 namespace Orc.SmartImage 

 6 { 

 7     using TPixel = Argb32; 

 8     using TCache = System.Int32; 

 9     using TKernel = System.Int32; 

10 

11     public static partial class ImageArgb32ClassHelper 

12     { 

13         #region include "ImageClassHelper_Template.cs" 

14         #endregion 

15     } 

16 

17     public partial class ImageArgb32 

18     { 

19         #region include "Image_Template.cs" 

20         #endregion 

21     } 

22 

23     public partial struct Argb32 

24     { 

25         #region include "TPixel_Template.cs" 

26         #endregion 

27     } 

28 } 


 

由于 Argb32 未提供和 int 之间的比较,因此,在这里 #region include "TPixel_Template.cs"。而Byte可以与int比较,因此,在ImageU8中,就不需要#region include "TPixel_Template.cs": 

C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理C#玩转指针(二):预处理器、using、partial关键字与region的妙用_预处理_02代码

 3 using System; 

 4 using System.Collections.Generic; 

 5 using System.Text; 

 6 

 7 namespace Orc.SmartImage 

 8 { 

 9     using TPixel = System.Byte; 

10     using TCache = System.Int32; 

11     using TKernel = System.Int32; 

12 

13     public static partial class ImageU8ClassHelper 

14     { 

15         #region include "ImageClassHelper_Template.cs" 

16         #endregion 

17     } 

18 

19     public partial class ImageU8 

20     { 

21         #region include "Image_Template.cs" 

22         #endregion 

23     } 

24 }


 


是不是很有意思呢?强大的指针,结合C#强大的语法和快速编译,至少在图像处理这一块是很好用的。

 

版权所有,欢迎转载

举报

相关推荐

0 条评论