欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

深入理解C#指针之美

程序员文章站 2022-03-12 08:21:07
目录二、c# 指针基础1、使用dispose模式管理非托管内存3、模拟c中的union(联合体)类型四、c# 指针操作的几个缺点七、风情万种的lambda表达式八、与c/c++的比较一、简洁优美的代码...

一、简洁优美的代码

本来初稿这节写了好几百字,将c#指针开发与c/c++开发,java开发、d语言开发等进行对比,阐述理念。不过现在觉得,阐述一个新事物,没有比用例子更直接的了。

例子:打开一张图像,先将它转化为灰度图像,再进行二值化(变成黑白图像),然后进行染色,将白色的像素变成红色。以上每一个过程都弹出窗体显示出来。

代码截图更有视觉冲击力:

深入理解C#指针之美

二、c# 指针基础

在c#中使用指针,需要在项目属性中选中“allow unsafe code”:

深入理解C#指针之美

接着,还需要在使用指针的代码的上下文中使用unsafe关键字,表明这是一段unsafe代码。可以用unsafe { } 将代码围住,如:

                     unsafe
                     {
                         new imageargb32(path).showdialog("原始图像")
                             .tograyscaleimage().showdialog("灰度图像")
                             .applyotsuthreshold().showdialog("二值化图像")
                             .toimageargb32()
                             .foreach((argb32* p) => { if (p->red == 255) *p = argb32.red; })
                             .showdialog("染色");
                     }

也可在方法或属性上加入unsafe关键字,如:

   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);

system.runtime.interopservices.marshal.allochglobal 用来从非托管堆上分配内存。system.runtime.interopservices.marshal.freehglobal(handle)用来释放从非托管对上分配的内存。这样我们就可以避开gc,自己管理内存了。

三、几种常用用法

1、使用dispose模式管理非托管内存

如果使用非托管内存,建议用dispose模式来管理内存,这样做有以下好处: 可以手动dispose来释放内存;可以使用using 关键字开管理内存;即使不释放,当dispose对象被gc回收时,也会收回内存。

下面是dispose模式的简单例子:

public unsafe class unmanagedmemory : idisposable
          {
              public int count { get; private set; }
              private byte* handle;
             private bool _disposed = false;
              public unmanagedmemory(int bytes)
              {
                 handle = (byte*) system.runtime.interopservices.marshal.allochglobal(bytes);
                 count = bytes;
            }
             public void dispose()
             {
                 dispose(true);
                 gc.suppressfinalize(true);
             }
            protected virtual void dispose( bool isdisposing )
             {
                 if (_disposed) return;
                 if (isdisposing)
                 {
                     if (handle != null)
                     {                         system.runtime.interopservices.marshal.freehglobal((intptr)handle);
                     }
                 }
                 _disposed = true;
             }
             ~unmanagedmemory()
            {
               dispose( false );
            }
         }

使用:

  using (unmanagedmemory memory = new unmanagedmemory(10))
            {
                int* p = (int*)memory.handle;
                *p = 20;
                messagebox.show(p->tostring());
            }

2、使用 stackalloc 在栈中分配内存

c# 提供了stackalloc 关键字可以直接在栈中分配内存,一般情况下,使用栈内存会比使用堆内存速度快,且栈内存不用担心内存泄漏。下面是例子:

       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 可以模拟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# 指针操作的缺点也不少。下面一一道来。

缺点1:只能用来操作值类型

.net中,引用类型的内存管理全部是由gc代劳,无法取得其地址,因此,无法用指针来操作引用类型。所以,c#中指针操作受到值类型的限制,其中,最主要的一点就是:值类型无法继承。

这一点看起来是致命的,其实不然。首先,需要用到指针来提高性能的地方,其类型是很少变动的。其次,在oo编程中有个名言:组合优于继承。使用组合,我们可以解决很多需要继承的地方。第三,最后,我们还可以使用引用类型来对值类型打包,进行继承,权衡两者的比重来完成任务。

缺点2:泛型不支持指针类型

c# 中泛型不支持指针类型。这是个很大的限制,在后面的篇幅中,我会引入模板机制来克服这个问题。同理,迭代器也不支持指针,因此,我们需要自己实现迭代机制。

缺点3:没有函数指针

幸运的是,c# 中有delegate,delegate 支持支持指针类型,lambda 表达式也支持指针。后面会详细讲解。

五、引入模板机制

没有泛型,但是我们可以模拟出一套类似c++的模板机制出来,进行代码复用。这里大量的用到了c#的语法糖和ide的支持。

先介绍原理:

partial 关键字让我们可以将一个类的代码分在多个文件,那么可以这样分:第一个文件是我们自己写的代码,第二个文件用来描述模板,第三个文件,用来根据模板自动生成代码。

三个文件这样取名字的:

深入理解C#指针之美

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.csimage_template.csimage_paramid_argb_templete.cstpixel_template.cs

只看其中的一个模板文件 image_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 文件也是可以编译通过的。在不同的模板定义中,令同样的符号来代表不同的类型,实现了模板代码的公用。

上面机制可以全部自动化。csmacro 是我写的一个工具,可以完成上面的过程。将它放在系统路径下,然后在项目的build event中添加pre-build 指令即可。csmacro程序在代码包的lib的目录下。

深入理解C#指针之美

如此实装,我们就有模板用了!一切自动化,就好像内置的一样。强类型、有编译器进行类型约束,减少出错的可能。调试也很容易,就和调试普通的c#代码一样,不存在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
     }
 }

我在命名空间中加入了 hide,只要不引用这个命名空间,这个扩展方法不会出现对程序产生干扰。

接着,建立 personclasshelper.cs 文件:

namespace introduce
 {
     using t = person;
     public static partial class personclasshelper
     {
         #region include "template.cs"
         #endregion 
     }
 }

建立 catclasshelper.cs 文件:

 namespace introduce
 {
     using t = cat;
     public static partial class catclasshelper
     {
         #region include "template.cs"
         #endregion
     }
 }

为了节省篇幅,我省略了命名空间的引用,实际代码中是有命名空间的引用的。下载包里包含了全部的代码。接下来,编译一下,哈哈,编译通过。

且慢,怎么看不到编译生成的两个 csmacro.cs 文件呢?

这两个文件已经生成了,需要手动将它们添加到项目中,只用添加一次即可。添加进来,再编译一下,哈哈,通过。

这个例子虽小,可不要小看模板啊,在geb.image库里,大量使用了模板:

深入理解C#指针之美

有了模板,只用维护公共代码。

六、迭代器

下面来实现迭代器。这里,要放弃使用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;
     }

测试性能和直接循环性能几乎一样。有人可能要问,你这样有什么优势?和for循环有什么区别?

这个例子中当然看不出优势,换个例子就可以看出来了。

在图像编程中,有 roi(region of interest,感兴趣区域)的概念。比如,在下面这张女王出场的画面中,假设我们只对她的头部感兴趣(roi区域),只对该区域进行处理(标注为红色区域)。

对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迭代器又可以产生一个itargb32迭代器,来迭代该行中的像素。

产生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迭代器进行迭代和直接进行循环,性能一致。为一副图像添加roi字段,设置roi值来控制不同的处理区域,然后用roi迭代器进行迭代,比直接使用循环要方便得多。

七、风情万种的lambda表达式

接下来,来看看c#指针最有风情的一面——lambda表达式。 c# 里 delegate 支持指针,下面这种写法是没有问题的:

 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解决。

做图像的话,很多时候需要使用显卡加速,如使用cuda或opencl,幸运的是,c#也可以直接写cuda或opencl代码,但是功能可能会受到所用的库的限制。也可以用传统方式写cuda或opencl代码,再p/invoke调用。如果用传统的c/c++开发的话,也需要做同样的工作。

和c比较:

这套方案比c的抽象程度高,我们有模板,有lambda表达式,还有一大票的语法糖。在类库上,比c的类库完善的多。我们还有反射,有命名空间等等一大票的东西。

和c++比较:

这套方案的抽象程度比c++要低一些。毕竟,值类型无法继承,模板机制比c++ 差一点。但是在生产力上比c++要高很多。抛开c++那一大票陷阱不说,以秒计算的编译速度就够让c++程序员流口水的。当我们在咖啡馆里约会喝咖啡时,c++程序员还正端着一杯咖啡坐在电脑前等待程序编译结束。

九、接下来的工作

接下来的工作主要有两个:

内联工具:c# 的内联还不够强大。需要一个内联工具,对想要内联的方法使用特性标记一下,在编译结束后,在il代码层面内联。

翻译工具:移动开发是个痛。如何将c#的代码翻译成c/c++的代码,在缺乏.net的运行时下运行?

这两个工作都不紧要。c#内联效果不好的地方(这种情况很少),可以手动内联。至于移动开发嘛,在哥的一云三端大计中,c# 的定位是云图像开发(c#+cuda),三端中,桌面运用是用c#和flash开发,web和移动应用使用flash开发,没有c#的事情。

c/c++ 呢?更没有它们的位置啦!不对,还是有的。用它们来开发flash应用的核心算法!够另类吧!

总结

本篇文章就到这里了,希望可以帮助到你,也希望你能够多多关注的更对内容!

相关标签: C# 指针