欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解C#指針之美

 更新時間:2021年07月08日 16:09:39   作者:xiaotie  
在C#中,有時候希望通過指針來操作內(nèi)存,這樣可以提高效率。我們可以用unsafe關(guān)鍵字修飾含有指針操作的程序段,感興趣的小伙伴可以參考一下,希望可以幫到你

一、簡潔優(yōu)美的代碼

本來初稿這節(jié)寫了好幾百字,將C#指針開發(fā)與C/C++開發(fā),Java開發(fā)、D語言開發(fā)等進(jìn)行對比,闡述理念。不過現(xiàn)在覺得,闡述一個新事物,沒有比用例子更直接的了。

例子:打開一張圖像,先將它轉(zhuǎn)化為灰度圖像,再進(jìn)行二值化(變成黑白圖像),然后進(jìn)行染色,將白色的像素變成紅色。以上每一個過程都彈出窗體顯示出來。

代碼截圖更有視覺沖擊力:

二、C# 指針基礎(chǔ)

在C#中使用指針,需要在項(xiàng)目屬性中選中“Allow unsafe code”:

接著,還需要在使用指針的代碼的上下文中使用unsafe關(guān)鍵字,表明這是一段unsafe代碼??梢杂胾nsafe { } 將代碼圍住,如:

                     unsafe
                     {
                         new ImageArgb32(path).ShowDialog("原始圖像")
                             .ToGrayscaleImage().ShowDialog("灰度圖像")
                             .ApplyOtsuThreshold().ShowDialog("二值化圖像")
                             .ToImageArgb32()
                             .ForEach((Argb32* p) => { if (p->Red == 255) *p = Argb32.RED; })
                             .ShowDialog("染色");
                     }

也可在方法或?qū)傩陨霞尤雞nsafe關(guān)鍵字,如:

   private unsafe void btnSubmit_Click(object sender, EventArgs e)

也可在class或struct 上加上unsafe 關(guān)鍵字,如:

public partial unsafe class FrmDemo1 : Form

指針配合fixed關(guān)鍵字可以操作托管堆上的值類型,如:

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

指針也可以操作非托管堆上的內(nèi)存,如:

         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 用來從非托管堆上分配內(nèi)存。System.Runtime.InteropServices.Marshal.FreeHGlobal(handle)用來釋放從非托管對上分配的內(nèi)存。這樣我們就可以避開GC,自己管理內(nèi)存了。

三、幾種常用用法

1、使用Dispose模式管理非托管內(nèi)存

如果使用非托管內(nèi)存,建議用Dispose模式來管理內(nèi)存,這樣做有以下好處: 可以手動dispose來釋放內(nèi)存;可以使用using 關(guān)鍵字開管理內(nèi)存;即使不釋放,當(dāng)Dispose對象被GC回收時,也會收回內(nèi)存。

下面是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 在棧中分配內(nèi)存

C# 提供了stackalloc 關(guān)鍵字可以直接在棧中分配內(nèi)存,一般情況下,使用棧內(nèi)存會比使用堆內(nèi)存速度快,且棧內(nèi)存不用擔(dān)心內(nèi)存泄漏。下面是例子:

       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(聯(lián)合體)類型

使用 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;
        }

這個和指針無關(guān),非unsafe環(huán)境下也可使用,有很多用途,比如,序列化和反序列化,求hash值 ……

四、C# 指針操作的幾個缺點(diǎn)

C# 指針操作的缺點(diǎn)也不少。下面一一道來。

缺點(diǎn)1:只能用來操作值類型

.Net中,引用類型的內(nèi)存管理全部是由GC代勞,無法取得其地址,因此,無法用指針來操作引用類型。所以,C#中指針操作受到值類型的限制,其中,最主要的一點(diǎn)就是:值類型無法繼承。

這一點(diǎn)看起來是致命的,其實(shí)不然。首先,需要用到指針來提高性能的地方,其類型是很少變動的。其次,在OO編程中有個名言:組合優(yōu)于繼承。使用組合,我們可以解決很多需要繼承的地方。第三,最后,我們還可以使用引用類型來對值類型打包,進(jìn)行繼承,權(quán)衡兩者的比重來完成任務(wù)。

缺點(diǎn)2:泛型不支持指針類型

C# 中泛型不支持指針類型。這是個很大的限制,在后面的篇幅中,我會引入模板機(jī)制來克服這個問題。同理,迭代器也不支持指針,因此,我們需要自己實(shí)現(xiàn)迭代機(jī)制。

缺點(diǎn)3:沒有函數(shù)指針

幸運(yùn)的是,C# 中有delegate,delegate 支持支持指針類型,lambda 表達(dá)式也支持指針。后面會詳細(xì)講解。

五、引入模板機(jī)制

沒有泛型,但是我們可以模擬出一套類似C++的模板機(jī)制出來,進(jìn)行代碼復(fù)用。這里大量的用到了C#的語法糖和IDE的支持。

先介紹原理:

partial 關(guān)鍵字讓我們可以將一個類的代碼分在多個文件,那么可以這樣分:第一個文件是我們自己寫的代碼,第二個文件用來描述模板,第三個文件,用來根據(jù)模板自動生成代碼。

三個文件這樣取名字的:

XXXClassHelper 是模板定義文件,XXXClassHelper_Csmacro.cs 是自動生成的模板實(shí)現(xiàn)代碼。

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 關(guān)鍵字,可以為一個類型取別名。使用 VS 的 #region 來定義所使用的模板文件的位置。上面這個文件中,引用了4個模板文件:ImageClassHelper_Template.csImage_Template.cs,Image_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 關(guān)鍵字來對使用的類型取別名,同時,在代碼中,有一段用 #region mixin #endregion 環(huán)繞的代碼。只需要寫一個工具,將模板文件中 #region mixin#endregion 環(huán)繞的代碼提取出來,替換到模板定義中 #region include "Image_Template.cs" 和 #endregion 之間,生成第三個文件 ClassHelper_Csmacro.cs 即可實(shí)現(xiàn)模板機(jī)制。由于都使用了 using 關(guān)鍵字對類型取別名,因此,ClassHelper_Csmacro.cs 文件也是可以編譯通過的。在不同的模板定義中,令同樣的符號來代表不同的類型,實(shí)現(xiàn)了模板代碼的公用。

上面機(jī)制可以全部自動化。Csmacro 是我寫的一個工具,可以完成上面的過程。將它放在系統(tǒng)路徑下,然后在項(xiàng)目的build event中添加pre-build 指令即可。Csmacro程序在代碼包的lib的目錄下。

如此實(shí)裝,我們就有模板用了!一切自動化,就好像內(nèi)置的一樣。強(qiáng)類型、有編譯器進(jìn)行類型約束,減少出錯的可能。調(diào)試也很容易,就和調(diào)試普通的C#代碼一樣,不存在C++中的模板的難調(diào)試問題。缺點(diǎn)嘛,就是沒有C++中模板的語法優(yōu)美,但是,也看的過去,至少比C中的宏好看多了是吧。

參照上面對模板的實(shí)現(xiàn),完全可以定義出一套C#的宏出來。沒這樣做,是因?yàn)闆]這個需求。

下面是一個完整的例子,為 Person 類和 Cat 類添加模板擴(kuò)展方法(非擴(kuò)展方法也可類似添加),由于這個方法有指針,無法用泛型實(shí)現(xiàn):

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,只要不引用這個命名空間,這個擴(kuò)展方法不會出現(xiàn)對程序產(chǎn)生干擾。

接著,建立 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
     }
 }

為了節(jié)省篇幅,我省略了命名空間的引用,實(shí)際代碼中是有命名空間的引用的。下載包里包含了全部的代碼。接下來,編譯一下,哈哈,編譯通過。

且慢,怎么看不到編譯生成的兩個 Csmacro.cs 文件呢?

這兩個文件已經(jīng)生成了,需要手動將它們添加到項(xiàng)目中,只用添加一次即可。添加進(jìn)來,再編譯一下,哈哈,通過。

這個例子雖小,可不要小看模板啊,在Geb.Image庫里,大量使用了模板:

有了模板,只用維護(hù)公共代碼。

六、迭代器

下面來實(shí)現(xiàn)迭代器。這里,要放棄使用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循環(huán)慢很多。對一個100萬像素的圖像,將其每一個像素值的Red分量設(shè)為200,循環(huán)100遍,使用迭代器在我的電腦上耗時242 ms,直接使用循環(huán)耗時 72 ms。我測試了很多種方案,均未得到和直接循環(huán)性能近似的迭代器實(shí)現(xiàn)方案。

沒有辦法,只好對迭代器來打折了,只進(jìn)行部分抽象(這已經(jīng)不能算迭代器了,但這里仍沿用這個名稱):

     public unsafe struct ItArgb32
     {
         public unsafe Argb32* Start;
         public unsafe Argb32* End;
         public int Step(Argb32* ptr)
         {
             return 1;
         }
     }

產(chǎn)生迭代器的代碼:

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

測試性能和直接循環(huán)性能幾乎一樣。有人可能要問,你這樣有什么優(yōu)勢?和for循環(huán)有什么區(qū)別?

這個例子中當(dāng)然看不出優(yōu)勢,換個例子就可以看出來了。

在圖像編程中,有 ROI(Region of Interest,感興趣區(qū)域)的概念。比如,在下面這張女王出場的畫面中,假設(shè)我們只對她的頭部感興趣(ROI區(qū)域),只對該區(qū)域進(jìn)行處理(標(biāo)注為紅色區(qū)域)。

對ROI區(qū)域創(chuàng)建一個迭代器,用來迭代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迭代器又可以產(chǎn)生一個ItArgb32迭代器,來迭代該行中的像素。

產(chǎn)生ROI迭代器的代碼如下,為了簡化代碼,我這里沒有進(jìn)行ROI的驗(yàn)證:

     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迭代器進(jìn)行迭代和直接進(jìn)行循環(huán),性能一致。為一副圖像添加ROI字段,設(shè)置ROI值來控制不同的處理區(qū)域,然后用ROI迭代器進(jìn)行迭代,比直接使用循環(huán)要方便得多。

七、風(fēng)情萬種的Lambda表達(dá)式

接下來,來看看C#指針最有風(fēng)情的一面——Lambda表達(dá)式。 C# 里 delegate 支持指針,下面這種寫法是沒有問題的:

 void ActionOnPixel(TPixel* p);

對于圖像處理,我定義了許多擴(kuò)展方法,F(xiàn)orEach是其中的一種,下面是它的模板定義:

     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表達(dá)式對圖像迭代,將每像素的Red分量設(shè)為200吧,一行代碼搞定:

img.ForEach((Argb32* p) => { p->Red = 200; });

用ForEach測試,對100萬像素的圖像設(shè)置Red通道值為200,循環(huán)100次,我的測試結(jié)果是 400 ms,約是直接循環(huán)的 4-5 倍??梢娺@是個性能不高的操作(其實(shí)也夠高了,100萬象素,循環(huán)100遍,耗時400ms),可以在對性能要求不是特別高時使用。

八、與C/C++的比較

我測試了很多場景,C# 下指針性能約是 C/C++ 的 70-80%,性能差距,可以忽略。

相對于C/C++來說,C#無法直接操作硬件是其遺憾,這種情況,可以使用C/C++寫段小程序來彌補(bǔ),不過,我還沒遇到這種場景。很多情況都可以P/Invoke解決。

做圖像的話,很多時候需要使用顯卡加速,如使用CUDA或OpenCL,幸運(yùn)的是,C#也可以直接寫CUDA或OpenCL代碼,但是功能可能會受到所用的庫的限制。也可以用傳統(tǒng)方式寫CUDA或OpenCL代碼,再P/Invoke調(diào)用。如果用傳統(tǒng)的C/C++開發(fā)的話,也需要做同樣的工作。

和C比較:

這套方案比C的抽象程度高,我們有模板,有l(wèi)ambda表達(dá)式,還有一大票的語法糖。在類庫上,比C的類庫完善的多。我們還有反射,有命名空間等等一大票的東西。

和C++比較:

這套方案的抽象程度比C++要低一些。畢竟,值類型無法繼承,模板機(jī)制比C++ 差一點(diǎn)。但是在生產(chǎn)力上比C++要高很多。拋開C++那一大票陷阱不說,以秒計(jì)算的編譯速度就夠讓C++程序員流口水的。當(dāng)我們在咖啡館里約會喝咖啡時,C++程序員還正端著一杯咖啡坐在電腦前等待程序編譯結(jié)束。

九、接下來的工作

接下來的工作主要有兩個:

內(nèi)聯(lián)工具:C# 的內(nèi)聯(lián)還不夠強(qiáng)大。需要一個內(nèi)聯(lián)工具,對想要內(nèi)聯(lián)的方法使用特性標(biāo)記一下,在編譯結(jié)束后,在IL代碼層面內(nèi)聯(lián)。

翻譯工具:移動開發(fā)是個痛。如何將C#的代碼翻譯成C/C++的代碼,在缺乏.Net的運(yùn)行時下運(yùn)行?

這兩個工作都不緊要。C#內(nèi)聯(lián)效果不好的地方(這種情況很少),可以手動內(nèi)聯(lián)。至于移動開發(fā)嘛,在哥的一云三端大計(jì)中,C# 的定位是云圖像開發(fā)(C#+CUDA),三端中,桌面運(yùn)用是用C#和Flash開發(fā),Web和移動應(yīng)用使用Flash開發(fā),沒有C#的事情。

C/C++ 呢?更沒有它們的位置啦!不對,還是有的。用它們來開發(fā)Flash應(yīng)用的核心算法!夠另類吧!

總結(jié)

本篇文章就到這里了,希望可以幫助到你,也希望你能夠多多關(guān)注腳本之家的更對內(nèi)容!

相關(guān)文章

  • C#查找列表中所有重復(fù)出現(xiàn)元素的方法

    C#查找列表中所有重復(fù)出現(xiàn)元素的方法

    這篇文章主要介紹了C#查找列表中所有重復(fù)出現(xiàn)元素的方法,涉及C#針對列表操作的技巧,非常具有實(shí)用價值,需要的朋友可以參考下
    2015-04-04
  • Unity?AssetPostprocessor模型函數(shù)Model實(shí)用案例深入解析

    Unity?AssetPostprocessor模型函數(shù)Model實(shí)用案例深入解析

    這篇文章主要為大家介紹了Unity?AssetPostprocessor模型Model函數(shù)實(shí)用案例深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • C#下使用XmlDocument操作XML詳解

    C#下使用XmlDocument操作XML詳解

    本文詳細(xì)講解了C#使用XmlDocument操作XML的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-06-06
  • Unity實(shí)現(xiàn)顏色漸變滑動條

    Unity實(shí)現(xiàn)顏色漸變滑動條

    這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)顏色漸變滑動條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-07-07
  • C#使用反射和LINQ查詢程序集的元數(shù)據(jù)

    C#使用反射和LINQ查詢程序集的元數(shù)據(jù)

    在?C#?中,反射是一個強(qiáng)大的工具,它允許我們在運(yùn)行時檢查程序集、類型、方法等的元數(shù)據(jù),結(jié)合?LINQ,我們可以用更簡潔和表達(dá)力強(qiáng)的方式處理這些信息,本文將詳細(xì)講解如何使用反射與?LINQ?查詢程序集的元數(shù)據(jù),需要的朋友可以參考下
    2024-08-08
  • c# 在windows中操作IIS設(shè)置FTP服務(wù)器的示例

    c# 在windows中操作IIS設(shè)置FTP服務(wù)器的示例

    這篇文章主要介紹了c# 在windows中操作IIS設(shè)置FTP服務(wù)器的示例,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下
    2021-03-03
  • c#后臺輸出javascript語句示例程序

    c#后臺輸出javascript語句示例程序

    一個很不錯的b/s前臺輸出彈出對話框、后臺寫javascript語句、后臺直接關(guān)閉web頁面及一個集成了常用驗(yàn)證的通用類,十分的方便。代碼如下
    2013-12-12
  • c# 在windows服務(wù)中 使用定時器實(shí)例代碼

    c# 在windows服務(wù)中 使用定時器實(shí)例代碼

    這篇文章主要介紹了c# 在windows服務(wù)中 使用定時器實(shí)例代碼,有需要的朋友可以參考一下
    2013-12-12
  • c#測試反射性能示例

    c#測試反射性能示例

    這篇文章主要介紹了c#測試反射性能示例,Activator.CreateInstance和AssemblyCreateInstance性能測試,需要的朋友可以參考下
    2014-03-03
  • c# 獲得本地ip地址的三種方法

    c# 獲得本地ip地址的三種方法

    這篇文章主要介紹了c# 獲得本地ip地址的三種方法,幫助大家更好的理解和實(shí)用c#,感興趣的朋友可以了解下
    2020-12-12

最新評論