将C#图像库的基础部分开源了(https://github.com/xiaotie/GebImage)。这个库比较简单,且离成熟还有一段距离,但它是一种新的开发模式的探索:以指针和非托管内存为主的C#程序开发。 一、简洁优美的代码 本来初稿这节写了好几百字,将C#指针开发与C/C++开发,Java开发、D语言开发等进行对比,阐述理念。不过现在觉得,阐述一个新事物,没有比用例子更直接的了。
二、C# 指针基础在C#中使用指针,需要在项目属性中选中“Allow unsafe code”: 接着,还需要在使用指针的代码的上下文中使用unsafe关键字,表明这是一段unsafe代码。 unsafe { new ImageArgb32(path).ShowDialog("原始图像") .ToGrayscaleImage().ShowDialog("灰度图像") .ApplyOtsuThreshold().ShowDialog("二值化图像") .ToImageArgb32() .ForEach((Argb32* p) => { if (p->Red == 255) *p = Argb32.RED; }) .ShowDialog("染色"); }
private unsafe void btnSubmit_Click(object sender, EventArgs e)
也可在class或struct 上加上unsafe 关键字,如: public partial unsafe class FrmDemo1 : Form 指针配合fixed关键字可以操作托管堆上的值类型,如: public unsafe class Person { public int Age; public void SetAge(int age) { fixed (int* p = &Age) { *p = age; } } } 指针可以操作栈上的值类型,如: int age = 0; int* p = &age; *p = 20; MessageBox.Show(p->ToString()); IntPtr handle = System.Runtime.InteropServices.Marshal.AllocHGlobal(4); Int32* p = (Int32*)handle; *p = 20; MessageBox.Show(p->ToString()); System.Runtime.InteropServices.Marshal.FreeHGlobal(handle);
三、几种常用用法1、使用Dispose模式管理非托管内存 如果使用非托管内存,建议用Dispose模式来管理内存,这样做有以下好处: 可以手动dispose来释放内存;可以使用using 关键字开管理内存;即使不释放,当Dispose对象被GC回收时,也会收回内存。 下面是Dispose模式的简单例子: ![]() 使用: using (UnmanagedMemory memory = new UnmanagedMemory(10)) { int* p = (int*)memory.Handle; *p = 20; MessageBox.Show(p->ToString()); }
int* p = stackalloc int[10]; for (int i = 0; i < 10; i++) { p[i] = 2 * i + 2; } MessageBox.Show(p[9].ToString());
3、模拟C中的union(联合体)类型 [StructLayout(LayoutKind.Explicit)] public struct Argb32 { [FieldOffset(0)] public Byte Blue; [FieldOffset(1)] public Byte Green; [FieldOffset(2)] public Byte Red; [FieldOffset(3)] public Byte Alpha; [FieldOffset(0)] public Int32 IntVal; } 这个和指针无关,非unsafe环境下也可使用,有很多用途,比如,序列化和反序列化,求hash值 …… 四、C# 指针操作的几个缺点 C# 指针操作的缺点也不少。下面一一道来。 五、引入模板机制 没有泛型,但是我们可以模拟出一套类似C++的模板机制出来,进行代码复用。这里大量的用到了C#的语法糖和IDE的支持。
XXXClassHelper 是模板定义文件,XXXClassHelper_Csmacro.cs 是自动生成的模板实现代码。 ClassHelper文件的例子: namespace Geb.Image { using TPixel = Argb32; using TCache = System.Int32; using TKernel = System.Int32; using TImage = Geb.Image.ImageArgb32; using TChannel = System.Byte; public static partial class ImageArgb32ClassHelper { #region include "ImageClassHelper_Template.cs" #endregion } public partial class ImageArgb32 { #region include "Image_Template.cs" #endregion #region include "Image_Paramid_Argb_Templete.cs" #endregion } public partial struct Argb32 { #region include "TPixel_Template.cs" #endregion } } 这里用到了using 语法糖。using 关键字,可以为一个类型取别名。使用 VS 的 #region 来定义所使用的模板文件的位置。上面这个文件中,引用了4个模板文件:ImageClassHelper_Template.cs,Image_Template.cs,Image_Paramid_Argb_Templete.cs 和 TPixel_Template.cs。 using TPixel = System.Byte; using TCache = System.Int32; using TKernel = System.Int32; using System; using System.Collections.Generic; using System.Text; namespace Geb.Image.Hidden { public abstract class Image_Template : UnmanagedImage<TPixel> { private Image_Template() : base(1,1) { throw new NotImplementedException(); } #region mixin public unsafe TPixel* Start { get { return (TPixel*)this.StartIntPtr; } } public unsafe TPixel this[int index] { get { return Start[index]; } set { Start[index] = value; } } …… #endregion } } 这个模板文件是编译通过的。也使用了 using 关键字来对使用的类型取别名,同时,在代码中,有一段用 #region mixin 和 #endregion 环绕的代码。只需要写一个工具,将模板文件中 #region mixin 和 #endregion 环绕的代码提取出来,替换到模板定义中 #region include "Image_Template.cs" 和 #endregion 之间,生成第三个文件 ClassHelper_Csmacro.cs 即可实现模板机制。由于都使用了 using 关键字对类型取别名,因此,ClassHelper_Csmacro.cs 文件也是可以编译通过的。在不同的模板定义中,令同样的符号来代表不同的类型,实现了模板代码的公用。 如此实装,我们就有模板用了!一切自动化,就好像内置的一样。强类型、有编译器进行类型约束,减少出错的可能。调试也很容易,就和调试普通的C#代码一样,不存在C++中的模板的难调试问题。缺点嘛,就是没有C++中模板的语法优美,但是,也看的过去,至少比C中的宏好看多了是吧。 下面是一个完整的例子,为 Person 类和 Cat 类添加模板扩展方法(非扩展方法也可类似添加),由于这个方法有指针,无法用泛型实现: void SetAge(this T item, int* age) 首先,建一个可编译通过的模板类 Template.cs: namespace Introduce.Hide { using T = Person; public static class Template { #region mixin public static unsafe void SetAge(this T item, int* age) { item.Age = *age; } #endregion } } namespace Introduce { using T = Person; public static partial class PersonClassHelper { #region include "Template.cs" #endregion } }
namespace Introduce { using T = Cat; public static partial class CatClassHelper { #region include "Template.cs" #endregion } } 这两个文件已经生成了,需要手动将它们添加到项目中,只用添加一次即可。添加进来,再编译一下,哈哈,通过。 有了模板,只用维护公共代码。 六、迭代器下面来实现迭代器。这里,要放弃使用foreach,返回古老的迭代器模式,来访问图像的每一个像素: public unsafe struct ItArgb32Old { public unsafe Argb32* Current; public unsafe Argb32* End; public unsafe Argb32* Next() { if (Current < End) return Current ++; else return null; } } public static class ImageArgb32Helper { public unsafe static ItArgb32Old CreateItorOld(this ImageArgb32 img) { ItArgb32Old itor = new ItArgb32Old(); itor.Current = img.Start; itor.End = img.Start + img.Length; return itor; } } 不幸的是,测试性能,这个迭代器比单纯的while循环慢很多。对一个100万像素的图像,将其每一个像素值的Red分量设为200,循环100遍,使用迭代器在我的电脑上耗时242 ms,直接使用循环耗时 72 ms。我测试了很多种方案,均未得到和直接循环性能近似的迭代器实现方案。 没有办法,只好对迭代器来打折了,只进行部分抽象(这已经不能算迭代器了,但这里仍沿用这个名称): public unsafe struct ItArgb32 { public unsafe Argb32* Start; public unsafe Argb32* End; public int Step(Argb32* ptr) { return 1; } } public unsafe static ItArgb32 CreateItor(this ImageArgb32 img) { ItArgb32 itor = new ItArgb32(); itor.Start = img.Start; itor.End = img.Start + img.Length; return itor; } 使用: ItArgb32 itor = img.CreateItor(); for (Argb32* p = itor.Start; p < itor.End; p+= itor.Step(p)) { p->Red = 200; }
对ROI区域创建一个迭代器,用来迭代ROI中的每一行: public unsafe struct ItRoiArgb32 { public unsafe Argb32* Start; public unsafe Argb32* End; public int Width; public int RoiWidth; public int Step(Argb32* ptr) { return Width; } public ItArgb32 Itor(Argb32* p) { ItArgb32 it = new ItArgb32(); it.Start = p; it.End = p + RoiWidth; return it; } } 产生ROI迭代器的代码如下,为了简化代码,我这里没有进行ROI的验证: public unsafe static ItRoiArgb32 CreateRoiItor(this ImageArgb32 img, int x, int y, int roiWidth, int roiHeight) { ItRoiArgb32 itor = new ItRoiArgb32(); itor.Width = img.Width; itor.RoiWidth = roiWidth; itor.Start = img.Start + img.Width * y + x; itor.End = itor.Start + img.Width * roiHeight; return itor; } 性能测试表明,使用ROI迭代器进行迭代和直接进行循环,性能一致。 七、风情万种的Lambda表达式 接下来,来看看C#指针最有风情的一面——Lambda表达式。 void ActionOnPixel(TPixel* p); 对于图像处理,我定义了许多扩展方法,ForEach是其中的一种,下面是它的模板定义: public unsafe static UnmanagedImage<TPixel> ForEach(this UnmanagedImage<TPixel> src, ActionOnPixel handler) { TPixel* start = (TPixel*)src.StartIntPtr; TPixel* end = start + src.Length; while (start != end) { handler(start); ++start; } return src; } 让我们用lambda表达式对图像迭代,将每像素的Red分量设为200吧,一行代码搞定: img.ForEach((Argb32* p) => { p->Red = 200; }); 用ForEach测试,对100万像素的图像设置Red通道值为200,循环100次,我的测试结果是 400 ms,约是直接循环的 4-5 倍。可见这是个性能不高的操作(其实也够高了,100万象素,循环100遍,耗时400ms),可以在对性能要求不是特别高时使用。 八、与C/C++的比较我测试了很多场景,C# 下指针性能约是 C/C++ 的 70-80%,性能差距,可以忽略。 相对于C/C++来说,C#无法直接操作硬件是其遗憾,这种情况,可以使用C/C++写段小程序来弥补,不过,我还没遇到这种场景。很多情况都可以P/Invoke解决。 和C比较: 这套方案比C的抽象程度高,我们有模板,有lambda表达式,还有一大票的语法糖。在类库上,比C的类库完善的多。我们还有反射,有命名空间等等一大票的东西。 和C++比较: 九、接下来的工作 接下来的工作主要有两个: C/C++ 呢?更没有它们的位置啦!不对,还是有的。用它们来开发Flash应用的核心算法!够另类吧! |