鬼画符 發表於 2025-5-12 22:45:00

理解 C# 中的各类指针

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>前言</li><li>对象引用(Object Reference)</li><li>指针(Pointer)<ul><li>指针的声明和使用</li><li>指针可以指向的位置</li><li>可以声明指针的位置</li><li>指向值类型变量的指针</li><li>指向对象引用的指针</li><li>指向 GC Heap 的指针</li><li>指向数组元素的指针</li><li>指向静态字段的指针</li><li>指向非托管内存的指针</li><li>作为方法参数的指针</li><li>作为方法返回值的指针</li><li>多级指针</li><li>进一步理解 fixed 关键字</li></ul></li><li>IntPtr<ul><li>基本概念</li><li>指向非托管内存的 IntPtr</li><li>保存句柄的 IntPtr</li></ul></li><li>函数指针(Function Pointer)<ul><li>基本概念</li><li>函数指针的声明和使用</li><li>托管函数指针和非托管函数指针</li></ul></li><li>托管指针(Managed Pointer)<ul><li>托管指针的声明和使用</li><li>托管指针可以指向的位置</li><li>可以声明托管指针的位置</li><li>托管指针的限制</li><li>指向对象引用的托管指针</li><li>指向 GC Heap 的托管指针</li><li>指向数组元素的托管指针</li><li>指向静态字段的托管指针</li><li>作为方法参数的托管指针</li><li>ref readonly 托管指针</li><li>作为 ref struct 的字段的托管指针</li><li>托管指针受 GC 管理</li><li>Unsafe.AsRef 方法</li></ul></li></ul></div><p></p>
<h1 id="前言">前言</h1>
<p>变量可以理解成是一块内存位置的别名,访问变量也就是访问对应内存中的数据。</p>
<p>指针是一种特殊的变量,它存储了一个内存地址,这个内存地址代表了另一块内存的位置。</p>
<p>指针指向的可以是一个变量、一个数组元素、一个对象实例、一块非托管内存、一个函数等。</p>
<p>截止到发文为止,.NET 最新正式版本为 .NET 9,C# 最新正式版本为 C# 13。文中提及的 <code>IL</code> 代码可能会随编译器版本的不同而有所差异,仅供参考。</p>
<p>本文将介绍到发文为止 C# 中的各类指针,并对比差异:</p>
<ul>
<li>
<p>对象引用(Object Reference)</p>
</li>
<li>
<p>指针(Pointer,一些资料中称为非托管指针)</p>
</li>
<li>
<p>IntPtr(表示指针或句柄的值,用于管理非托管资源或非托管代码交互)</p>
</li>
<li>
<p>函数指针(Function Pointer)</p>
</li>
<li>
<p>托管指针(Managed Pointer)</p>
</li>
</ul>
<p>本文旨在为读者建立对各类指针的概念认知,不会每个细节都展开,读者可以参考 C# 的官方文档,了解更多用法。</p>
<p>涉及的知识点较多,如果存在纰漏和错误,还请谅解。</p>
<h1 id="对象引用object-reference">对象引用(Object Reference)</h1>
<p>对象引用,也就是我们常说的引用类型变量,是一个类型安全的指针,指向引用类型实例的 MethodTable 指针,通过偏移和计算可以访问对象头和字段。</p>
<p><img src="https://img2023.cnblogs.com/blog/1201123/202505/1201123-20250512224303898-1876930133.png" alt="" loading="lazy"></p>
<p>对象实例被分配在托管堆上,引用类型变量存储了一个指向该对象实例的引用。对象引用可以被赋值为 null,表示没有指向任何对象实例。通过 null 的对象引用访问不存在的对象会导致 <code>NullReferenceException</code>。</p>
<p>对象引用可以存在栈或者堆上,作为局部变量时,存储在栈上;作为值类型字段时,跟随值类型的位置存储;作为引用类型字段时,存储在堆上。</p>
<h1 id="指针pointer">指针(Pointer)</h1>
<h2 id="指针的声明和使用">指针的声明和使用</h2>
<p>指针允许用户直接操作内存地址,提供了更高的性能和灵活性,但也带来了更高的风险。因此,C# 只允许在用 <code>unsafe</code> 关键字标记的代码块中使用指针,并且需要在项目中启用 <code>&lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt;</code>。</p>
<p><code>unsafe</code> 关键字可以用于方法、代码块、字段、类、结构体等。</p>
<p>一些资料中将这边的指针(Pointer)称为非托管指针(Unmanaged Pointer),因为它们不受 <code>GC</code> 的管理。</p>
<p>我们需要使用 <code>&lt;type&gt;* ptr</code> 的语法来声明指针类型的变量。</p>
<p>通过 <code>&amp;</code> 运算符获取变量的地址,通过 <code>*</code> 运算符访问指针指向的数据。</p>
<p><code>&amp;</code> 通常被称为寻址运算符,<code>*</code> 通常被称为解引用运算符或间接寻址运算符。</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      int* p = null; // 声明一个指向 int 的指针
      int a = 10;
      p = &amp;a; // 获取 a 的地址并赋值给指针 p
      Console.WriteLine(*p); // 输出 10
    }
}
</code></pre>
<h2 id="指针可以指向的位置">指针可以指向的位置</h2>
<p>指针可以指向以下几种位置:</p>
<ul>
<li>
<p>值类型变量:也就是指向值类型的数据本体。</p>
</li>
<li>
<p>引用类型变量:因为引用类型变量存储的是对象实例的引用,所以这边相当于一个二级指针。</p>
</li>
<li>
<p>值类型或者引用类型的实例字段:readonly 也可以修改。</p>
</li>
<li>
<p>值类型或者引用类型的静态字段:readonly 也可以修改。</p>
</li>
<li>
<p>数组元素:数组在内存中是连续存储的,所以可以通过指针和指针算法来访问数组元素。</p>
</li>
<li>
<p>非托管内存:使用 <code>Marshal</code> 分配非托管内存。</p>
</li>
<li>
<p>另一个指针(Pointer):可以实现多级指针。</p>
</li>
<li>
<p>null:表示没有指向任何有效的内存地址,通过 null 指针访问不存在的数据会导致 <code>NullReferenceException</code>。</p>
</li>
</ul>
<p><strong>注意:在声明指向实例字段,静态字段以及数组元素的指针时,需要使用 <code>fixed</code> 关键字。</strong></p>
<h2 id="可以声明指针的位置">可以声明指针的位置</h2>
<p>指针可以在以下位置声明:</p>
<ul>
<li>
<p>局部变量:可以在方法中声明指针变量。</p>
</li>
<li>
<p>方法参数:可以将指针作为方法参数传递。</p>
</li>
<li>
<p>方法返回值:可以将指针作为方法的返回值。</p>
</li>
<li>
<p>实例字段:可以在类或结构体中声明指针类型的字段。</p>
</li>
<li>
<p>静态字段:可以在类或者结构体中声明指针类型的静态字段。</p>
</li>
<li>
<p>只读属性:包含只读索引(indexer),但不支持自动属性(Automatically implemented properties)。</p>
</li>
</ul>
<h2 id="指向值类型变量的指针">指向值类型变量的指针</h2>
<p>指针可以指向值类型变量,直接访问值类型的数据本体,并且可以修改值类型变量的值。</p>
<p><img src="https://img2023.cnblogs.com/blog/1201123/202505/1201123-20250512224303477-1228482318.png" alt="" loading="lazy"></p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      int a = 10;
      int* p = &amp;a; // 获取 a 的地址并赋值给指针 p
      Console.WriteLine(*p); // 输出 10

      *p = 20; // 修改指针 p 指向的值
      Console.WriteLine(a); // 输出 20
    }
}
</code></pre>
<h2 id="指向对象引用的指针">指向对象引用的指针</h2>
<p>指针可以指向对象引用,相当于一个二级指针。</p>
<p><img src="https://img2023.cnblogs.com/blog/1201123/202505/1201123-20250512224303087-33976861.png" alt="" loading="lazy"></p>
<p>在下面的示例代码中,关键的部分标注了编译后的 IL 代码。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      var foo = new Foo
      {
            Bar = 1
      };

      unsafe
      {
            // ldloca.s   foo   // 加载 foo 的地址
            // conv.u             // 将 foo 的地址转换为 unsigned native int
            // stloc.1            // 将转换后的 int 存储到 fooPtr
            Foo* fooPtr = &amp;foo;

            // ldloc.1            // 加载 fooPtr
            // ldind.ref          // 将 fooPtr 指向的对象引用加载到栈上
            // callvirt   instance int32 Foo::get_Bar()
            // call         void System.Console::WriteLine(int32)
            Console.WriteLine(fooPtr-&gt;Bar); // 输出 1

            // ldloc.1            // 加载 fooPtr
            // newobj       instance void Foo::.ctor()
            // dup
            // ldc.i4.2
            // callvirt   instance void Foo::set_Bar(int32)
            // nop
            // stind.ref          // 新的 Foo 对象的地址保存通过 fooPtr 保存到 foo
            *fooPtr = new Foo
            {
                Bar = 2
            };

            // ldloc.0      // 和指针相比,少了一个 ldind.ref,对象引用可以直接使用
            // callvirt   instance int32 Foo::get_Bar()
            // call         void System.Console::WriteLine(int32)
            Console.WriteLine(foo.Bar); // 输出 2
            
            // ldloc.1      // 加载 fooPtr
            // ldind.ref    // 将 fooPtr 指向的对象引用加载到栈上
            // ldc.i4.3   // 将 3 压入栈上
            // callvirt   instance void Foo::set_Bar(int32)
            fooPtr-&gt;Bar = 3;
            Console.WriteLine(foo.Bar); // 输出 3
      }
    }
}

class Foo
{
    public int Bar { get; set; }
}
</code></pre>
<p>关键的三个IL 指令:</p>
<ul>
<li>
<p><code>conv.u</code>:将对象引用(foo)的地址转换为 unsigned native int,并存储到指针(fooPtr)中。</p>
</li>
<li>
<p><code>ldind.ref</code>:将指针(fooPtr)指向的对象引用(foo)加载到栈上。</p>
</li>
<li>
<p><code>stind.ref</code>:将栈上的对象引用(新的foo实例的引用)存储到指针指向的地址(foo)上。</p>
</li>
</ul>
<p><img src="https://img2023.cnblogs.com/blog/1201123/202505/1201123-20250512224302703-916170210.png" alt="" loading="lazy"></p>
<h2 id="指向-gc-heap-的指针">指向 GC Heap 的指针</h2>
<p>如果指针指向 GC Heap 上的数据,例如指向数组元素或者引用类型实例字段,指针需要通过 <code>fixed</code> 关键字固定对象的地址,防止 <code>GC</code> 移动对象的位置。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      Foo foo = new Foo
      {
            Bar = 1
      };

      unsafe
      {
            fixed (int* p = &amp;foo.Bar) // 固定 foo.Bar 的地址
            {
                Console.WriteLine(*p); // 输出 1

                *p = 2; // 修改指针 p 指向的值
            }
      }

      Console.WriteLine(foo.Bar); // 输出 2
    }
}

class Foo
{
    public int Bar;
}
</code></pre>
<p><strong>注意:不应在 <code>fixed</code> 语句块结束后,继续使用指针变量,因为 <code>GC</code> 可能会移动对象的位置,导致指针指向无效的内存地址。</strong></p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      Foo foo = new Foo
      {
            Bar = 1
      };

      var weakReference = new WeakReference(foo);

      unsafe
      {
            int* p2;
            fixed (int* p1 = &amp;foo.Bar) // 固定 foo.Bar 的地址
            {
                Console.WriteLine(*p1); // 输出 1

                p2 = p1; // 将指针 p1 存放的地址复制给 指针p2

                *p1 = 2; // 修改指针 p1 指向的值
            }

            Console.WriteLine(*p2); // 输出 2,此时 p1 已经被释放了,但 p2 仍然可以访问到 foo.Bar 的值

            // 往托管堆上分配一些数据,并触发 GC
            for (int i = 0; i &lt; 1_000_000; i++)
            {
                var arr = new int;
            }

            GC.Collect();

            Console.WriteLine(weakReference.IsAlive); // 输出 true,证明 foo 仍然存活
            Console.WriteLine(*p2); // 输出 0, 因为 foo 的位置已经被 GC 移动了
      }
    }
}

class Foo
{
    public int Bar;
}
</code></pre>
<h2 id="指向数组元素的指针">指向数组元素的指针</h2>
<p>当指针指向数组元素时,可以通过指针算法遍历数组元素,指针的单次偏移量为元素类型的大小。</p>
<p><img src="https://img2023.cnblogs.com/blog/1201123/202505/1201123-20250512224301911-1219750162.png" alt="" loading="lazy"></p>
<p>指针算法支持的操作有:</p>
<p>对指针进行加法和减法运算时,p + n 是将指针 p 向后移动 n 个元素的大小,p - n 是将指针 p 向前移动 n 个元素的大小。</p>
<p>本文会讨论三种数组类型:</p>
<ul>
<li>在栈上分配的数组</li>
<li>在托管堆上分配的数组</li>
<li>在非托管堆上分配的数组</li>
</ul>
<p>本小节先讨论前两种,指向非托管堆上分配的数组的指针会在后面讨论。</p>
<p>栈上和非托管堆上分配的数组时,指针可以直接访问数组元素。在托管堆上分配的数组时,指针需要通过 <code>fixed</code> 关键字固定数组元素的地址,防止 <code>GC</code> 移动数组元素的位置。</p>
<p>在栈上分配的数组的示例代码:</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {

      int* arr = stackalloc int { 0, 1, 2, 3, 4 }; // 在栈上分配一个 int 数组并初始化
      // 下面是等效代码
      // int* arr = stackalloc int; // 在栈上分配一个 int 数组
      // for (int i = 0; i &lt; 5; i++)
      // {
      //   *(arr + i) = i; // 通过指针访问数组元素,赋值
      // }
      for (int i = 0; i &lt; 5; i++)
      {
            Console.WriteLine(*(arr + i)); // 输出 0 1 2 3 4
      }
      // 也可以直接通过下标访问
      for (int i = 0; i &lt; 5; i++)
      {
            Console.WriteLine(arr); // 输出 0 1 2 3 4
      }
    }
}
</code></pre>
<p>在托管堆上分配的数组的示例代码:</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      int[] arr = new int { 0, 1, 2, 3, 4 }; // 在堆上分配一个 int 数组并初始化
      fixed (int* p = arr) // 固定数组元素的地址
      {
            for (int i = 0; i &lt; 5; i++)
            {
                Console.WriteLine(*(p + i)); // 输出 0 1 2 3 4
            }
      }

      fixed (int* p = &amp;arr) // 固定数组元素的地址
      {
            for (int i = 0; i &lt; 5; i++)
            {
                *(p + i) = i * 10; // 修改数组元素的值
            }
      }

      foreach (var item in arr)
      {
            Console.WriteLine(item); // 输出 0 10 20 30 40
      }
    }
}
</code></pre>
<p>在 <code>fixed</code> 语句块结束后,数组元素的地址会被释放,指针变量将不再有效。</p>
<p>在 <code>fixed</code> 语句块中,指针变量可以直接访问数组元素的地址,并且可以修改数组元素的值。</p>
<p><code>int* p = arr</code> 和 <code>int* p = &amp;arr</code> 是等效的,都是获取数组第一个元素的地址。</p>
<p><strong>注意: <code>int[]* p = &amp;arr</code> 是创建一个指向数组变量的指针,并不是指向数组元素的指针。</strong></p>
<h2 id="指向静态字段的指针">指向静态字段的指针</h2>
<p>静态字段位于托管堆上,但非 <code>GC</code> 管理的内存区域,理论上内存地址应该是固定的,但不排除某些平台实现或某些情况下会被移动。</p>
<p>在.NET的规范以及C#语言规范中,编译器并不能完全确定某个字段是否可移动,必须通过 <code>fixed</code> 修饰保证安全。</p>
<p>统一使用 <code>fixed</code> 也可以避免特例导致的复杂性或bug。如果静态保存的是值类型还好。但如果静态字段保存的是一个对象引用,那就和方法的局部变量一样,指针必定需要通过 <code>fixed</code> 关键字固定对象的地址,防止 <code>GC</code> 移动对象的位置。静态字段如果存的是数组的引用,也是必须使用 <code>fixed</code> 关键字固定对象的地址才能访问数组元素。</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      // 值类型的静态字段
      Foo.ValueTypeField = 1;

      // 获取指针
      fixed (int* valueTypeFieldPtr = &amp;Foo.ValueTypeField)
      {
            *valueTypeFieldPtr = 2; // 修改值类型字段的值
      }

      Console.WriteLine(Foo.ValueTypeField); // 输出 2

      // 引用类型的静态字段
      Foo.ReferenceTypeField = new Bar { Baz = 1 };

      // 获取指针
      fixed (Bar* referenceTypeFieldPtr = &amp;Foo.ReferenceTypeField)
      {
            *referenceTypeFieldPtr = new Bar { Baz = 2 }; // 修改引用类型字段的值
      }

      Console.WriteLine(Foo.ReferenceTypeField.Baz); // 输出 2

      // 数组的静态字段
      Foo.ArrayField = ;

      // 获取指针
      fixed (int* arrayFieldPtr = Foo.ArrayField)
      {
            arrayFieldPtr = 4; // 修改数组的值
      }

      Console.WriteLine(Foo.ArrayField); // 输出 4
    }
}

class Foo
{
    public static int ValueTypeField;

    public static Bar ReferenceTypeField;

    public static int[] ArrayField;
}

class Bar
{
    public int Baz;
}
</code></pre>
<h2 id="指向非托管内存的指针">指向非托管内存的指针</h2>
<p>使用 <code>Marshal.AllocHGlobal</code> 分配非托管内存,返回一个指向非托管内存的指针,最后使用 <code>Marshal.FreeHGlobal</code> 释放非托管内存。</p>
<p><code>Marshal</code> 提供的方法的参数和返回值都是 <code>IntPtr</code> 类型,但可以和指针互换转换。</p>
<pre><code class="language-csharp">public static class Marshal
{
    public static IntPtr AllocHGlobal(int cb);
    public static void FreeHGlobal(IntPtr hglobal);
}
</code></pre>
<pre><code class="language-csharp">using System.Runtime.InteropServices;

unsafe class Program
{
    static void Main()
    {
      // 在非托管内存中分配一块内存用于存储整数数组
      int size = 10;
      var ptr = (int*)Marshal.AllocHGlobal(size * sizeof(int));

      // 将数据写入非托管内存
      for (int i = 0; i &lt; size; i++)
      {
            ptr = i;
      }

      // 读取非托管内存的数据
      for (int i = 0; i &lt; size; i++)
      {
            Console.WriteLine(ptr);
      }

      // 也可以使用指针算法访问非托管内存存储的数组
      // int* p = ptr;
      // for (int i = 0; i &lt; size; i++)
      // {
      //   Console.WriteLine(*p);
      //   p++;
      // }

      // 释放非托管内存
      Marshal.FreeHGlobal((IntPtr)ptr);
    }
}
</code></pre>
<h2 id="作为方法参数的指针">作为方法参数的指针</h2>
<p>指针可以作为方法参数传递,允许在方法中修改指针指向的数据,但指针本身的传递是值传递,无法在传入的方法中修改指针的值,也就是无法修改指针指向的地址。</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      int a = 10;
      int b = 20;
      
      int* p1 = &amp;a; // 获取 a 的地址并赋值给指针 p1
      int* p2 = &amp;b; // 获取 b 的地址并赋值给指针 p2
      Console.WriteLine(*p1); // 输出 10
      Console.WriteLine(*p2); // 输出 20

      ModifyPointer(p1, p2); // 传递指针 p1 和 p2
      Console.WriteLine(*p1); // 输出 11
    }

    static void ModifyPointer(int* p1, int* p2)
    {
      *p1 = 11; // 修改指针 p1 指向的值
      
      p1 = p2; // 无效代码,不会影响外部的 p1
    }
}
</code></pre>
<h2 id="作为方法返回值的指针">作为方法返回值的指针</h2>
<p><strong>当指针作为方法的返回值时,需要注意不能返回局部变量的指针,因为局部变量在方法结束后会被销毁,指针将指向无效的内存地址。</strong></p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      Foo* p = GetPointer(); // 获取指针

      Console.WriteLine(p-&gt;Bar); // 输出 10
      Console.WriteLine(p-&gt;Bar); // 输出 随机值
    }

    static Foo* GetPointer()
    {
      Foo a = new Foo
      {
            Bar = 10
      };
      return &amp;a;
    }
}

struct Foo
{
    public int Bar;
}
</code></pre>
<p>上述代码中,<code>GetPointer</code> 方法返回了一个指向局部变量 <code>a</code> 的指针,但 <code>a</code> 在方法结束后会被销毁,所以返回的指针将指向无效的内存地址。</p>
<p>之所以第一次输出 10,是因为 <code>a</code> 的内存数据没有被覆盖,第二次输出随机值是因为 <code>a</code> 的内存数据已经被覆盖。</p>
<p>在打印 <code>p-&gt;Bar</code> 之前,将一些别的数据载入到栈上,就会覆盖 <code>a</code> 的内存数据。下面的代码只打印了一次 <code>p-&gt;Bar</code>,但在打印之前,已经将 20 到过栈上(被 <code>Console.WriteLine</code> 消费了),所以 <code>a</code> 的内存数据被覆盖了。</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      Foo* p = GetPointer(); // 获取指针
      Console.WriteLine(20); // 输出 20
      Console.WriteLine(p-&gt;Bar); // 输出 随机值
    }

    static Foo* GetPointer()
    {
      Foo a = new Foo
      {
            Bar = 10
      };
      return &amp;a;
    }
}

struct Foo
{
    public int Bar;
}
</code></pre>
<p>改为返回字段的指针也是一样的结果</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      int* p = GetPointer(); // 获取指针

      Console.WriteLine(*p); // 输出 10
      Console.WriteLine(*p); // 输出 随机值
    }

    static int* GetPointer()
    {
      Foo a = new Foo
      {
            Bar = 10
      };
      return &amp;a.Bar;
    }
}

struct Foo
{
    public int Bar;
}
</code></pre>
<h2 id="多级指针">多级指针</h2>
<p>下面是一个三级指针的例子</p>
<pre><code class="language-csharp">{
    int x = 1;
    int* p1 = &amp;x;         // 一级指针
    int** p2 = &amp;p1;       // 二级指针
    int*** p3 = &amp;p2;      // 三级指针
   
    ***p3 = 2;            // 三次寻址

    Console.WriteLine(x); // 输出 2
}
</code></pre>
<h2 id="进一步理解-fixed-关键字">进一步理解 fixed 关键字</h2>
<p><code>fixed</code> 关键字用于固定对象的地址,防止 <code>GC</code> 移动对象的位置。</p>
<p>查看下面代码编译成的 IL 代码。</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      // 引用类型的静态字段
      Foo.ReferenceTypeField = new Bar { Baz = 1 };

      // 获取指针
      fixed (Bar* referenceTypeFieldPtr = &amp;Foo.ReferenceTypeField)
      {
            *referenceTypeFieldPtr = new Bar { Baz = 2 }; // 修改引用类型字段的值
      }

      Console.WriteLine(Foo.ReferenceTypeField.Baz); // 输出 2

      // 数组的静态字段
      Foo.ArrayField = ;

      // 获取指针
      fixed (int* arrayFieldPtr = Foo.ArrayField)
      {
            arrayFieldPtr = 4; // 修改数组的值
      }

      Console.WriteLine(Foo.ArrayField); // 输出 4
    }
}

class Foo
{
    public static Bar ReferenceTypeField;

    public static int[] ArrayField;
}

class Bar
{
    public int Baz;
}
</code></pre>
<pre><code class="language-il">.class private auto ansi beforefieldinit
Program
    extends System.Object
{

.method private hidebysig static void
    Main() cil managed
{
    .entrypoint
    .maxstack 4
    .locals init (
       class Bar* referenceTypeFieldPtr,
       class Bar&amp; pinned V_1,
       int32* arrayFieldPtr,
       int32[] pinned V_3
    )
    // ... 省略方法体
}
}
</code></pre>
<p>在 IL 代码中,<code>Bar&amp; pinned V_1</code> 和 <code>int32[] pinned V_3</code> 表示固定的指向对象引用的托管指针和固定的数组的对象引用。</p>
<p><code>pinned</code> 表示这个对象引用是固定的,<code>GC</code> 会识别到这个标记,并不会移动其指向的对象的位置。</p>
<p>在 <code>fixed</code> 语句块内,对 <code>Bar* referenceTypeFieldPtr</code> 的读写将转换为 <code>Bar&amp; pinned V_1</code> 的读写。对 <code>int32* arrayFieldPtr</code> 的读写将转换为 <code>int32[] pinned V_3</code> 的读写。</p>
<h1 id="intptr">IntPtr</h1>
<h2 id="基本概念">基本概念</h2>
<p><code>IntPtr</code> 是一个结构体,表示指针或句柄的值,用于管理非托管资源或非托管代码交互。</p>
<p>在部分场景,可以和指针互换使用,但 <code>IntPtr</code> 不能直接进行指针运算。</p>
<p><code>IntPtr</code> 是一个平台相关的类型,在 32 位平台上是 4 字节,在 64 位平台上是 8 字节。</p>
<p>在使用 <code>IntPtr</code> 时,不需要使用 <code>unsafe</code> 关键字,也不需要启用 <code>&lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt;</code>(如果使用 P/Invoke 调用非托管函数时,仍然需要启用)。</p>
<h2 id="指向非托管内存的-intptr">指向非托管内存的 IntPtr</h2>
<p>在使用 <code>IntPtr</code> 管理非托管内存时,不能直接读取和写入内存,需要使用 <code>Marshal</code> 提供的<code>ReadXXX</code> 和 <code>WriteXXX</code> 方法。</p>
<pre><code class="language-csharp">using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
      // 在非托管内存中分配一块内存用于存储整数数组
      int size = 10;
      IntPtr ptr = Marshal.AllocHGlobal(size * sizeof(int));
      
      // 将数据写入非托管内存
      for (int i = 0; i &lt; size; i++)
      {
            Marshal.WriteInt32(ptr + i * sizeof(int), i);
      }
      
      // 读取非托管内存的数据
      for (int i = 0; i &lt; size; i++)
      {
            Console.WriteLine(Marshal.ReadInt32(ptr + i * sizeof(int)));
      }
      
      // 释放非托管内存
      Marshal.FreeHGlobal(ptr);
    }
}
</code></pre>
<h2 id="保存句柄的-intptr">保存句柄的 IntPtr</h2>
<p><code>IntPtr</code> 也可以用于存储句柄,例如文件句柄、窗口句柄等。</p>
<p>句柄可以理解为一个指向资源的引用,通常是一个整数值,用于唯一标识和访问由操作系统管理的资源。本质上它是一个资源标识符,而不是资源在内存中的实际地址。</p>
<p>下面是一个 windows 平台的例子</p>
<pre><code class="language-csharp">using System.Runtime.InteropServices;

public static partial class Program
{
    // Define a delegate that corresponds to the unmanaged function.
    private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);

    // Import user32.dll (containing the function we need) and define
    // the method corresponding to the native function.
   
    private static partial int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);

    // Define the implementation of the delegate; here, we simply output the window handle.
    private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
    {
      Console.WriteLine(hwnd.ToInt64());
      return true;
    }

    public static void Main(string[] args)
    {
      // Invoke the method; note the delegate as a first parameter.
      EnumWindows(OutputWindow, IntPtr.Zero);
    }
}
</code></pre>
<p>上面的代码使用了 <code>LibraryImport</code> 特性来导入 <code>user32.dll</code> 中的 <code>EnumWindows</code> 函数,并定义了一个委托 <code>EnumWC</code> 来对应这个函数的回调函数。<code>EnumWindows</code> 函数会枚举所有顶级窗口,并调用 <code>OutputWindow</code> 函数来输出每个窗口的句柄。</p>
<p><code>OutputWindow</code> 函数的参数 <code>hwnd</code> 是一个 <code>IntPtr</code> 类型的句柄,表示窗口的句柄。可以使用 <code>hwnd.ToInt64()</code> 将其转换为长整型值进行输出。</p>
<h1 id="函数指针function-pointer">函数指针(Function Pointer)</h1>
<h2 id="基本概念-1">基本概念</h2>
<p>函数指针是一个指向函数的指针,分为托管函数指针和非托管函数指针。</p>
<p>这是一个 C# 9 新增的特性,建议读者阅读官方文档地址加深理解:<br>
https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers</p>
<p>在 IL 层面,调用方法的指令分为三种:</p>
<ul>
<li>
<p><code>call</code>:直接调用静态方法或非虚方法。</p>
<ul>
<li>常用于静态方法、私有实例方法、构造函数、基类方法等。</li>
<li>不会进行虚方法表查找,故不能用于虚方法调用。</li>
</ul>
</li>
<li>
<p><code>callvirt</code>:用于调用虚方法(virtual)、接口方法,或者有时也用来调用非虚实例方法。</p>
<ul>
<li>会进行虚方法表(vtable)查找,确保调用最终派生类的实现(多态)。</li>
<li>调用前自动检测 this 是否为 null,如果是则抛出 NullReferenceException。所以 C# 编译器的常见做法是对非虚方法也使用 <code>callvirt</code>,以保证 null 检查。</li>
</ul>
</li>
<li>
<p><code>calli</code>:间接调用,通过函数指针进行调用。</p>
<ul>
<li>性能开销更低,但安全性、类型检查弱。</li>
<li>通常只有在编写 IL 代码,或者使用 Emit 动态生成代码时才会使用。</li>
<li>新增的函数指针语法允许在 C# 中使用 <code>calli</code> 指令,提供了更好的类型安全性。</li>
</ul>
</li>
</ul>
<p>早期 C# 为我们提供了委托(Delegate)来封装方法的引用,委托可以看作是一个类型安全的函数指针。所有的委托类型都继承自 <code>System.Delegate</code> 类。我们在调用委托时,实际上是调用了委托的 <code>Invoke</code> 这个虚方法,IL 指令是 <code>callvirt</code>。</p>
<p>在后期新增的函数指针语法中,编译器使用 <code>calli</code> 指令来调用函数,而不是实例化委托对象并调用 <code>Invoke</code> 方法。</p>
<h2 id="函数指针的声明和使用">函数指针的声明和使用</h2>
<p>和指针一样,函数指针也需要在 <code>unsafe</code> 代码块中使用,并且需要启用 <code>&lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt;</code>。</p>
<p>声明函数指针的语法如下:</p>
<pre><code class="language-csharp">delegate*&lt;, return type&gt; variableName
</code></pre>
<p><code>delegate*</code> 是一个关键字,表示函数指针类型。</p>
<p><code>&lt;parameter type list&gt;</code> 是参数类型列表,可以是空的,也可以是一个或多个参数类型,用逗号分隔。<br>
<code>return type</code> 是返回值类型,可以是 <code>void</code> 或者其他类型。</p>
<p>下面是几个例子:</p>
<ul>
<li>
<p><code>delegate*&lt;void&gt; ptr</code>:表示一个不带参数和返回值的函数指针。</p>
</li>
<li>
<p><code>delegate*&lt;int&gt; ptr</code>:表示一个不带参数,返回值为 <code>int</code> 的函数指针。</p>
</li>
<li>
<p><code>delegate*&lt;int, int, int&gt; ptr</code>:表示一个带两个 <code>int</code> 参数,返回值为 <code>int</code> 的函数指针。</p>
</li>
<li>
<p><code>delegate*&lt;int, int, void&gt; ptr</code>:表示一个带两个 <code>int</code> 参数,无返回值的函数指针。</p>
</li>
</ul>
<p>函数指针的声明和使用示例:</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      // 声明一个函数指针,指向一个返回 int 的函数,参数为两个 int
      delegate*&lt;int, int, int&gt; addPtr = &amp;Add;

      // 调用函数指针
      int result = addPtr(1, 2);
      Console.WriteLine(result); // 输出 3
    }

    static int Add(int a, int b)
    {
      return a + b;
    }
}
</code></pre>
<p>使用 <code>&amp;</code> 运算符获取函数的地址,并赋值给函数指针变量。</p>
<p>函数指针只能指向静态方法,不能指向实例方法或者委托。</p>
<p>可以指向静态的本地函数(local function),也就是说这个本地函数不是闭包。</p>
<p>下面对比函数指针和委托,用 <code>BenchmarkDotNet</code> 做个简单的性能测试</p>
<pre><code class="language-csharp">public class Program
{
    public static void Main(string[] args)
    {
      BenchmarkRunner.Run&lt;Benchmark&gt;();
    }
}


public class Benchmark
{
    private delegate int AddDelegate(int a, int b);
    private static AddDelegate addDelegate = Add;

    private unsafe delegate*&lt;int, int, int&gt; addPtr = &amp;Add;

   
    public void Delegate()
    {
      for (int i = 0; i &lt; 1000000; i++)
      {
            var result = addDelegate(1, 2);
      }
    }

   
    public unsafe void FunctionPointer()
    {
      for (int i = 0; i &lt; 1000000; i++)
      {
            var result = addPtr(1, 2);
      }
    }

    private static int Add(int a, int b)
    {
      return a + b;
    }
}
</code></pre>
<p>运行结果如下:</p>
<pre><code class="language-log">| Method          | Mean   | Error   | StdDev    | Allocated |
|---------------- |---------:|----------:|----------:|----------:|
| Delegate      | 1.530 ms | 0.0054 ms | 0.0048 ms |       1 B |
| FunctionPointer | 1.409 ms | 0.0042 ms | 0.0039 ms |       1 B |
</code></pre>
<p>虽然此处例子差距不是很明显,但还是能看到函数指针的性能更好一些。</p>
<h2 id="托管函数指针和非托管函数指针">托管函数指针和非托管函数指针</h2>
<p>在声明函数指针时,可以在 <code>delegate*</code> 后面加上 <code>managed</code> 或 <code>unmanaged</code> 关键字,表示托管函数指针或非托管函数指针。</p>
<p>不加关键字时,默认是托管函数指针。</p>
<p>下面是一个可以在 macOS 上运行的例子:</p>
<pre><code class="language-csharp">unsafe class Program
{
    // 声明C函数指针类型(C的 getpid:int getpid(void);)
    private delegate* unmanaged&lt;int&gt; GetPidDelegate;

    static void Main()
    {
      var prog = new Program();
      prog.Run();
    }

    public void Run()
    {
      // 加载libc(macOS下通常路径就是 /usr/lib/libc.dylib)
      IntPtr lib = NativeLibrary.Load("/usr/lib/libc.dylib");

      // 获取getpid符号
      IntPtr pidFuncPtr = NativeLibrary.GetExport(lib, "getpid");

      // 转为函数指针(需要unsafe上下文)
      GetPidDelegate = (delegate* unmanaged&lt;int&gt;)pidFuncPtr;

      // 用C#的函数指针调用 (unsafe 上下文中)
      int pid = GetPidDelegate();

      Console.WriteLine($"Current PID from libc.getpid(): {pid}");

      // 释放库
      NativeLibrary.Free(lib);
    }
}
</code></pre>
<p>上面的代码中,<code>delegate* unmanaged&lt;int&gt;</code> 声明了一个非托管函数指针类型,指向一个返回 <code>int</code> 的函数。</p>
<p><code>Cdecl</code> 是调用约定,表示使用 C 语言的调用约定。</p>
<p>通过获取 <code>getpid</code> 函数的地址,并将其转换为函数指针类型,最后调用该函数获取当前进程的 PID。</p>
<p><code>NativeLibrary</code> 是一个用于加载和调用非托管库的类,提供了 <code>Load</code> 和 <code>GetExport</code> 方法来加载库和获取函数地址。</p>
<p>使用完后,使用 <code>NativeLibrary.Free</code> 方法释放库。</p>
<h1 id="托管指针managed-pointer">托管指针(Managed Pointer)</h1>
<h2 id="托管指针的声明和使用">托管指针的声明和使用</h2>
<p>托管指针并非一个新的特性,在早期的 C# 版本中,我们在方法参数上使用的 <code>ref</code> 和 <code>out</code> 就是声明了托管指针。</p>
<p>在 IL 中,用 <code>&lt;type&gt;*</code> 来表示前面说的指针(pointer,有些资料中称为 非托管指针)。</p>
<p>而 <code>ref</code> 和 <code>out</code> 在 IL 中对应的是 <code>&lt;type&gt;&amp;</code>,也就是托管指针(managed pointer)。</p>
<p><code>out</code> 相当于 <code>ref</code> 的一种特殊情况,表示参数是一个输出参数,方法内部必须对其赋值。</p>
<p>另外还有一个 <code>in</code> 可以把方法参数声明为只读的托管指针,方法内部不能对其赋值。</p>
<p>使用托管指针时,我们不需要使用 <code>unsafe</code> 关键字,也不需要启用 <code>&lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt;</code>。</p>
<p><strong>注意:托管指针相关的语法会在几个位置用到 <code>ref</code> 关键字,但作用和意义是不同的。</strong></p>
<ul>
<li>
<p>我们使用 <code>ref &lt;type&gt; ptr</code> 来声明一个托管指针。</p>
</li>
<li>
<p>同时也用 <code>ref</code> 关键字来获取变量的地址,<code>ref &lt;type&gt; ptr = ref a</code>。</p>
</li>
<li>
<p>访问托管指针指向的数据时,语法上只需直接访问不带 <code>ref</code> 的指针变量名 <code>ptr</code> 即可。</p>
</li>
<li>
<p>复制托管指针的值时,需要在指针变量前面加上 <code>ref</code> 关键字。<code>ref &lt;type&gt; ptr2 = ref ptr</code>。</p>
</li>
<li>
<p>修改托管指针指向的数据时,语法上只需直接访问不带 <code>ref</code> 的指针变量名 <code>ptr</code> 即可,<code>ptr = ref b</code>。</p>
</li>
</ul>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      int a = 10;
      ref int p1 = ref a; // 声明一个托管指针,指向变量 a 的地址
      Console.WriteLine(p1); // 输出 10,访问托管指针 p1 指向的值,即 a 的值

      p1 = 20; // 修改托管指针 p1 指向的值,即修改 a 的值
      Console.WriteLine(a); // 输出 20

      ref int p2 = ref p1; // 将托管指针 p1 的值复制给 p2,即 p2 也指向 a 的地址

      p2 = 30; // 修改托管指针 p2 指向的值,即修改 a 的值
      Console.WriteLine(a); // 输出 30

      int b = 40;
      p1 = ref b; // 将 p1 重新指向 b
      Console.WriteLine(p1); // 输出 40,访问托管指针 p1 指向的值,即 b 的值
      
      p1 = 50; // 修改托管指针 p1 指向的值,即修改 b 的值
      Console.WriteLine(b); // 输出 50
      Console.WriteLine(p2); // 输出 30,p2 仍然指向 a 的地址
    }
}
</code></pre>
<h2 id="托管指针可以指向的位置">托管指针可以指向的位置</h2>
<ul>
<li>
<p>值类型变量:也就是指向值类型的数据本体。</p>
</li>
<li>
<p>引用类型变量:和上文指向对象引用的指针(Pointer)一样,相当于一个二级指针,但不支持指向另一个托管指针。</p>
</li>
<li>
<p>值类型或者引用类型的实例字段。</p>
</li>
<li>
<p>值类型或者引用类型的静态字段</p>
</li>
<li>
<p>数组元素:但不支持指针算法。</p>
</li>
<li>
<p>null:表示没有指向任何有效的内存地址,尝试访问 null 指针会导致 <code>NullReferenceException</code>。目前只有作为 <code>ref struct</code> 的 <code>ref</code> 字段时,可能出现这个情况,需使用 <code>Unsafe.IsNullRef&lt;T&gt;(T)</code> 方法确定 ref 字段是否为 null。</p>
</li>
</ul>
<h2 id="可以声明托管指针的位置">可以声明托管指针的位置</h2>
<ul>
<li>
<p>局部变量:可以在方法中声明托管指针变量。</p>
</li>
<li>
<p>方法参数:可以将托管指针作为方法参数传递。</p>
</li>
<li>
<p>方法返回值:可以将托管指针作为方法的返回值。</p>
</li>
<li>
<p>ref struct 的实例字段:<code>ref struct</code> 的 <code>ref</code> 不代表这种 <code>struct</code> 是按引用传递的,是指其具有类似托管指针的限制。</p>
</li>
<li>
<p>只读属性:包含只读索引(indexer),但不支持自动属性(Automatically implemented properties)。</p>
</li>
</ul>
<h2 id="托管指针的限制">托管指针的限制</h2>
<p>出于安全的设计目的,相较于指针(Pointer),托管指针只允许存在于栈上,不允许在存在于堆上。主要的限制如下:</p>
<ul>
<li>
<p>不能作为类或者非 <code>ref struct</code> 的结构体的字段。</p>
</li>
<li>
<p>不能作为静态字段,因为静态字段在保存在托管堆上(非 GC Heap)。</p>
</li>
<li>
<p>不能作为 async方法 或 迭代器方法 的参数,因为参数会被状态机捕获,并保存在堆上。</p>
</li>
<li>
<p>不能在 await 和 yield 语句中使用,因为相关的变量会被状态机捕获,并保存在堆上。</p>
</li>
<li>
<p>不能被闭包捕获,因为编译器会将闭包转换为一个类,并将捕获的变量作为类的字段。</p>
</li>
</ul>
<p>作为能保存托管指针的的 <code>ref struct</code>,也只允许在栈上分配内存。C# 对 <code>ref struct</code> 的限制主要如下:</p>
<ul>
<li>
<p>不能作为类或者非 <code>ref struct</code> 的结构体的字段。</p>
</li>
<li>
<p>不能作为静态字段。</p>
</li>
<li>
<p>不能装箱。无法将 <code>ref struct</code> 装箱为 <code>object</code> 或者接口类型。也无法将 <code>ref struct</code> 作为数组元素。</p>
</li>
<li>
<p>不能作为 async方法 的参数,因为参数会被状态机捕获,并保存在堆上。但可以作为迭代器方法的参数。</p>
</li>
<li>
<p>不能在 await 和 yield 语句中使用,因为相关的变量会被状态机捕获,并保存在堆上。</p>
</li>
<li>
<p>不能被闭包捕获,因为编译器会将闭包转换为一个类,并将捕获的变量作为类的字段。</p>
</li>
</ul>
<h2 id="指向对象引用的托管指针">指向对象引用的托管指针</h2>
<p>托管指针指向对象引用时,和指针(Pointer)一样,都类似于一个二级指针。</p>
<p>下面是一个简单的例子,演示了如何使用托管指针指向对象引用:</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      Foo foo = new Foo
      {
            Bar = 1
      };

      // 声明一个托管指针,指向 foo 的地址
      // ldloca.s   foo   // 加载 foo 的地址
      // stloc.1            // 将转换后的 int 存储到 fooPtr
      ref Foo fooPtr = ref foo;

      // 访问托管指针指向的对象引用
      // ldloc.1            // 加载 fooPtr
      // ldind.ref          // 将 fooPtr 指向的对象引用加载到栈上
      // callvirt   instance int32 Foo::get_Bar()
      // call         void System.Console::WriteLine(int32)
      Console.WriteLine(fooPtr.Bar); // 输出 1

      // 修改托管指针指向的对象引用
      // ldloc.1            // 加载 fooPtr
      // newobj       instance void Foo::.ctor()
      // dup
      // ldc.i4.2
      // callvirt   instance void Foo::set_Bar(int32)
      // nop
      // stind.ref          // 新的 Foo 对象的地址保存通过 fooPtr 保存到 foo
      fooPtr = new Foo
      {
            Bar = 2
      };

      // 访问托管指针指向的对象引用
      Console.WriteLine(foo.Bar); // 输出 2

      // 通过托管指针修改原对象的属性
      // ldloc.1      // 加载 fooPtr
      // ldind.ref    // 将 fooPtr 指向的对象引用加载到栈上
      // ldc.i4.3   // 将 3 压入栈上
      // callvirt   instance void Foo::set_Bar(int32)
      // nop
      fooPtr.Bar = 3;
      Console.WriteLine(foo.Bar); // 输出 3
    }
}

public struct Foo
{
    public int Bar { get; set; }
}
</code></pre>
<p>上面的代码中,<code>ref Foo fooPtr = ref foo;</code> 声明了一个托管指针 <code>fooPtr</code>,指向 <code>foo</code> 的地址。</p>
<p><code>fooPtr</code> 是一个托管指针,指向 <code>foo</code> 的地址,虽然语法可以直接访问 <code>fooPtr.Bar</code> 的属性,但其过程是先将 <code>fooPtr</code> 指向的对象引用加载到栈上,然后调用 <code>get_Bar()</code> 方法获取属性值。</p>
<p><code>fooPtr = new Foo { Bar = 2 };</code> 修改了 <code>fooPtr</code> 指向的对象引用,也就是修改了 <code>foo</code> 的值。</p>
<p>和指针(Pointer)那一章节生成的 IL 代码进行对比,你会发现,唯一的区别是将变量地址保存到指针时,指针比托管指针多了一个 <code>conv.u</code> 指令。</p>
<pre><code class="language-csharp">class Program
{
    static unsafe void Main()
    {
      Foo foo = new Foo
      {
            Bar = 1
      };

      // ldloca.s   foo
      // conv.u       // 将 foo 的地址转换为unsigned native int
      // stloc.1      // fooPtr1
      Foo* fooPtr1 = &amp;foo;

      // ldloca.s   foo
      // stloc.2      // fooPtr2
      ref Foo fooPtr2 = ref foo;
    }
}

public struct Foo
{
    public int Bar { get; set; }
}
</code></pre>
<p>可以看出唯一的区别就是 指针(Pointer)和托管指针(Managed Pointer)在保存变量地址时,指针(Pointer)需要转换为 unsigned native int,而托管指针(Managed Pointer)不需要转换。</p>
<p>在获取对象引用时 <code>ldind.ref</code> 同时支持两种指针格式。</p>
<h2 id="指向-gc-heap-的托管指针">指向 GC Heap 的托管指针</h2>
<p>托管指针受 GC 管理,不用关注指向的数据是否在 GC 过程中被移动。在 GC 过程中,托管指针会被自动更新为新的地址。</p>
<p>下面是一个简单的例子,演示了如何使用托管指针指向引用类型的实例字段:</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      Foo foo = new Foo
      {
            Bar = 1
      };

      ref int p = ref foo.Bar; // 声明一个托管指针,指向 foo 的 Bar 字段

      Console.WriteLine(p); // 输出 1

      p = 2; // 修改托管指针 p 指向的值,即修改 foo 的 Bar 字段

      Console.WriteLine(foo.Bar); // 输出 2
    }
}

public class Foo
{
    public int Bar;
}
</code></pre>
<h2 id="指向数组元素的托管指针">指向数组元素的托管指针</h2>
<p>托管指针可以指向数组元素,但不支持指针算法。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      int[] arr = new int { 0, 1, 2, 3, 4 };

      // 声明一个托管指针,指向数组的第一个元素
      ref int p = ref arr;

      Console.WriteLine(p); // 输出 0

      p = 10; // 修改托管指针 p 指向的值,即修改数组的第一个元素

      Console.WriteLine(arr); // 输出 10
    }
}
</code></pre>
<h2 id="指向静态字段的托管指针">指向静态字段的托管指针</h2>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      // 声明一个托管指针,指向静态字段 Foo.StaticField 的地址
      ref int p = ref Foo.StaticField;

      Console.WriteLine(p); // 输出 0

      p = 20; // 修改托管指针 p 指向的值,即修改 Foo.StaticField 的值

      Console.WriteLine(Foo.StaticField); // 输出 20
    }
}

public class Foo
{
    public static int StaticField;
}
</code></pre>
<h2 id="作为方法参数的托管指针">作为方法参数的托管指针</h2>
<p>目前,我们有下面几种方法可以声明托管指针作为方法参数:</p>
<p><strong>注意:托管指针本身是值传递,无法在方法内修改外部的托管指针的指向</strong></p>
<ol>
<li>
<p><code>ref</code> 关键字:表示参数是一个引用类型的托管指针,方法内部可以修改托管指针指向的外部变量。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      int a = 10;
      int b = 20;
      
      ref int p1 = ref a; // 声明一个托管指针,指向变量 a 的地址
      ref int p2 = ref b; // 声明一个托管指针,指向变量 b 的地址

      Modify(ref p1, ref p2); // 传递托管指针作为参数

      Console.WriteLine(a); // 输出 11
      Console.WriteLine(b); // 输出 22
    }

    static void Modify(ref int p1, ref int p2)
    {
      p1 = 11; // 修改托管指针 p1 指向的变量 a 的值

      p1 = ref p2; // 将托管指针 p1 指向变量 b 的地址,但托管指针本身是值传递的,不会影响原变量 a 的值,这边修改的只是作为参数的 p1 的值
      
      p1 = 22; // 修改托管指针 p1 指向的变量 b 的值
    }
    }
</code></pre>
</li>
<li>
<p><code>in</code> 关键字:表示参数是一个只读的托管指针,方法内部不能修改托管指针指向的外部变量。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      int a = 10;
      int b = 20;
      
      ref int p1 = ref a; // 声明一个托管指针,指向变量 a 的地址
      ref int p2 = ref b; // 声明一个托管指针,指向变量 b 的地址

      Modify(ref p1, ref p2); // 传递托管指针作为参数

      Console.WriteLine(a); // 输出 10
      Console.WriteLine(b); // 输出 20
    }

    static void Modify(in int p1, in int p2)
    {
      // p1 = 11; // 错误:不能修改 in 托管指针指向的变量 a 的值
      p1 = ref p2; // 无效:不能修改 in 托管指针 ref int p1 的指向
    }
}
</code></pre>
</li>
<li>
<p><code>out</code> 关键字:表示参数是一个输出参数,方法内部必须通过托管指针对其指向的外部变量赋值。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      int a = 10;
      int b = 20;

      Modify(out a, out b); // 传递托管指针作为参数

      Console.WriteLine(a);
      Console.WriteLine(b);
    }

    static void Modify(out int p1, out int p2)
    {
      p1 = 11; // 修改 p1 指向的变量 a 的值,不赋值会报错
      p2 = 22; // 修改 p2 指向的变量 b 的值,不赋值会报错

      p1 = ref p2; // 无效:不能修改 out 托管指针 ref int p1 的指向
    }
}
</code></pre>
</li>
<li>
<p><code>readonly ref</code> 关键字:按目前的标准,作为参数时和 <code>in</code> 关键字的效果是一样的。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      int a = 10;

      ref int p = ref a; // 声明一个托管指针,指向变量 a 的地址

      ModifyRef(ref p);
      ModifyRefReadonly(ref p);
      ModifyInt(in p);
    }

    static void ModifyRef(ref int p)
    {
      Console.WriteLine(p); // 可以读取托管指针指向的变量的值
      p = 11; // 修改托管指针指向的变量的值
    }

    static void ModifyInt(in int p)
    {
      Console.WriteLine(p); // 可以读取 in 托管指针指向的变量的值
      p = 11; // 错误:不能修改 in 托管指针指向的变量的值
    }

    static void ModifyRefReadonly(ref readonly int p)
    {
      Console.WriteLine(p); // 可以读取 ref readonly 托管指针指向的变量的值
      p = 11; // 错误:不能修改 in 托管指针指向的变量的值
    }
}
</code></pre>
</li>
</ol>
<h2 id="ref-readonly-托管指针">ref readonly 托管指针</h2>
<p>在声明作为局部变量的托管指针时,可以使用 <code>ref readonly</code> 关键字,表示无法通过这个托管指针修改其指向的数据,但是可以修改托管指针的指向。</p>
<pre><code class="language-csharp">class Program
{
    static void Main()
    {
      int a = 10;

      // 声明一个 ref readonly 托管指针,指向变量 a 的地址
      ref readonly int p1 = ref a;

      // p1 = 20; // 错误:无法修改指向的变量的值

      int b = 20;

      p1 = ref b; // 可以指向其他变量

      Console.WriteLine(p1); // 输出 20
      Console.WriteLine(a); // 输出 10,a 的值没有改变
    }
}
</code></pre>
<h2 id="作为-ref-struct-的字段的托管指针">作为 ref struct 的字段的托管指针</h2>
<p><code>ref struct</code> ref struct 并非表示引用传递的结构体,而是表示具有类似于托管指针的限制,依然和普通的结构体一样是值传递。</p>
<p>在 <code>ref struct</code> 可以声明托管指针作为字段。</p>
<p><strong>注意:只能在 <code>ref struct</code> 的构造函数中对 <code>ref 字段</code> 进行初始化,不支持初始化器初始化或者实例化完成之后的初始化,否则将触发 <code>NullReferenceException</code>。</strong></p>
<pre><code class="language-csharp">using System.Runtime.CompilerServices;

var foo = new Foo();

// 不能用 == null 来判断,会触发 NullReferenceException
// Console.WriteLine(foo.Value == null);

// 只能用 Unsafe.IsNullRef 来判断
Console.WriteLine(Unsafe.IsNullRef(foo.Value));

// 不能在 ref struct 实例化完成之后对 ref 字段进行初始化,会触发 NullReferenceException
// foo.Value = 1;

// 只能在 ref struct 的构造函数中对 ref 字段进行初始化
int value = 1;
var bar = new Bar(ref value);

Console.WriteLine(bar.Value);

ref struct Foo
{
    public ref int Value;
}

ref struct Bar
{
    public Bar(ref int value)
    {
      Value = ref value;
    }

    public ref int Value;
}
</code></pre>
<p>有几种方式可以声明 <code>ref struct</code> 的字段:</p>
<ol>
<li>
<p><code>ref</code> 关键字:表示字段是一个引用类型的托管指针,可以修改指针指向的数据以及修改指针的指向。</p>
<pre><code class="language-csharp">var a = 1;
var foo = new Foo(ref a);

Console.WriteLine(foo.Value); // 输出 1

// 修改指针指向的数据
foo.Value = 11;

Console.WriteLine(a); // 输出 11

// 修改指针的指向
var b = 2;

// 将指针重新指向 b
foo.Value = ref b;

Console.WriteLine(foo.Value); // 输出 2

ref struct Foo
{
    // 声明一个托管指针,指向 int 类型的值
    public ref int Value;

    public Foo(ref int value)
    {
      // 在构造函数中初始化托管指针
      Value = ref value;
    }
}
</code></pre>
</li>
<li>
<p><code>ref readonly</code> 关键字:表示字段是一个指向只读数据的托管指针,不能修改指针指向的数据,但可以修改指针的指向。</p>
<pre><code class="language-csharp">var a = 1;
var foo = new Foo(ref a);

Console.WriteLine(foo.Value); // 输出 1

// foo.Value = 11; // 编译错误:不能修改只读数据

// 修改指针的指向
var b = 2;
// 将指针重新指向 b
foo.Value = ref b;
Console.WriteLine(foo.Value); // 输出 2

ref struct Foo
{
    // 声明一个指向只读数据的托管指针,指向 int 类型的值
    public ref readonly int Value;

    public Foo(ref int value)
    {
      // 在构造函数中初始化托管指针
      Value = ref value;
    }
}
</code></pre>
</li>
<li>
<p><code>readonly ref</code> 关键字:表示字段是一个只读的托管指针,不能修改指针的指向,但可以修改指针指向的数据。</p>
<pre><code class="language-csharp">var a = 1;
var foo = new Foo(ref a);

Console.WriteLine(foo.Value); // 输出 1

// 修改指针指向的数据
foo.Value = 11;
Console.WriteLine(a); // 输出 11

// 修改指针的指向
var b = 2;
// 将指针重新指向 b
// foo.Value = ref b; // 编译错误:不能修改只读指针的指向

ref struct Foo
{
    // 声明一个只读的托管指针,指向 int 类型的值
    public readonly ref int Value;

    public Foo(ref int value)
    {
      // 在构造函数中初始化托管指针
      Value = ref value;
    }
}
</code></pre>
</li>
<li>
<p><code>readonly ref readonly</code> 关键字:表示字段是一个指向只读数据的只读托管指针,不能修改指针的指向,也不能修改指针指向的数据。</p>
<pre><code class="language-csharp">var a = 1;
var foo = new Foo(ref a);

Console.WriteLine(foo.Value); // 输出 1

// foo.Value = 11; // 编译错误:不能修改只读数据

int b = 2;
// 将指针重新指向 b
// foo.Value = ref b; // 编译错误:不能修改只读指针的指向

ref struct Foo
{
    // 声明一个指向只读数据的只读托管指针,指向 int 类型的值
    public readonly ref readonly int Value;

    public Foo(ref int value)
    {
      // 在构造函数中初始化托管指针
      Value = ref value;
    }
}
</code></pre>
</li>
</ol>
<h2 id="托管指针受-gc-管理">托管指针受 GC 管理</h2>
<p>托管指针受 GC 管理,不用关注指向的数据是否在 GC 过程中被移动。在 GC 过程中,托管指针会被自动更新为新的地址。</p>
<p>下面的例子中演示了用 指针(Pointer)和 托管指针(Managed Pointer)分别指向数组元素的情况。</p>
<p><code>GetArrayElementPointer</code> 方法中的数组对象在方法结束后失去了根引用,GC 会在下一次回收时将其回收。</p>
<p><code>GetArrayElementManagedPointer</code> 方法中的数组对象在方法结束后仍然<strong>有托管指针作为根引用</strong>,GC 不会回收它。</p>
<pre><code class="language-csharp">unsafe class Program
{
    static void Main()
    {
      Console.WriteLine("before GC");

      // 获取指针
      int* p1 = GetArrayElementPointer(out var wr1);

      // 输出 true,表示数组对象仍然存在
      Console.WriteLine($"wr1.IsAlive: {wr1.IsAlive}");
      // 输出 1
      Console.WriteLine($"*p1: {*p1}");

      // 获取托管指针
      ref int p2 = ref GetArrayElementManagedPointer(out var wr2);

      // 输出 true,表示数组对象仍然存在
      Console.WriteLine($"wr2.IsAlive: {wr2.IsAlive}");
      // 输出 2
      Console.WriteLine($"p2: {p2}");

      GC.Collect();

      Console.WriteLine();
      Console.WriteLine("after GC");

      // 输出 false,表示数组对象已被回收
      Console.WriteLine($"wr1.IsAlive: {wr1.IsAlive}");
      // 输出 随机值,有可能是 0,也有可能是其他值
      Console.WriteLine($"*p1: {*p1}");

      // 输出 true,表示数组对象仍然存在
      Console.WriteLine($"wr2.IsAlive: {wr2.IsAlive}");
      // 输出 2
      Console.WriteLine($"p2: {p2}");
    }

    static int* GetArrayElementPointer(out WeakReference wr)
    {
      int[] arr = ;

      wr = new WeakReference(arr);

      fixed (int* p = &amp;arr)
      {
            return p;
      }
    }

    static ref int GetArrayElementManagedPointer(out WeakReference wr)
    {
      int[] arr = ;

      wr = new WeakReference(arr);

      return ref arr;
    }
}
</code></pre>
<h2 id="unsafeasref-方法">Unsafe.AsRef 方法</h2>
<p><code>Unsafe.AsRef&lt;T&gt;</code> 有两个重载:</p>
<ol>
<li>
<p><code>AsRef&lt;T&gt;(Void*)</code>: 将非托管指针转换为指向 类型的 T值的托管指针。</p>
<pre><code class="language-csharp">using System.Runtime.CompilerServices;

unsafe class Program
{
    static void Main()
    {
      int a = 10;
      int* p = &amp;a;

      // 将非托管指针转换为指向 int 的托管指针
      ref int p1 = ref Unsafe.AsRef&lt;int&gt;(p);

      Console.WriteLine(p1); // 输出 10

      p1 = 20; // 修改托管指针 p1 指向的值,即修改 a 的值

      Console.WriteLine(a); // 输出 20
    }
}
</code></pre>
</li>
<li>
<p><code>AsRef&lt;T&gt;(T)</code>: 将给定的 <code>ref readonly</code> 托管指针重新解释为可以修改指向的值的托管指针。</p>
<p>可以修改 <code>ref readonly</code> 托管指针指向的值。</p>
<pre><code class="language-csharp">using System.Runtime.CompilerServices;

class Program
{
    static void Main()
    {
      int a = 10;

      // 声明一个 ref readonly 托管指针,指向变量 a 的地址
      ref readonly int p1 = ref a;

      // 将 ref readonly 托管指针转换为普通的托管指针
      ref int p2 = ref Unsafe.AsRef&lt;int&gt;(p1);

      Console.WriteLine(p2); // 输出 10

      p2 = 20; // 修改托管指针 p2 指向的值,即修改 a 的值

      Console.WriteLine(a); // 输出 20
      Console.WriteLine(p1); // 输出 20,p1 仍然指向 a 的地址
    }
}
</code></pre>
<p>也可以修改 <code>ref struct</code> 的 <code>ref readonly</code> 或 <code>readonly ref readonly</code> 字段的值。</p>
<pre><code class="language-csharp">using System.Runtime.CompilerServices;

var a = 1;
var foo = new Foo(ref a);

Console.WriteLine(foo.Value); // 输出 1

ref int p = refUnsafe.AsRef(foo.Value); // 获取指向 foo.Value 的指针

p = 11; // 修改指针指向的值

Console.WriteLine(a); // 输出 11

ref struct Foo
{
    // 声明一个指向只读数据的只读托管指针,指向 int 类型的值
    public readonly ref readonly int Value;

    public Foo(ref int value)
    {
      // 在构造函数中初始化托管指针
      Value = ref value;
    }
}
</code></pre>
</li>
</ol>
<p>欢迎关注个人技术公众号<br>
<img src="https://img2023.cnblogs.com/blog/1201123/202303/1201123-20230302194546214-138980196.png" alt="" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/eventhorizon/p/18873400
頁: [1]
查看完整版本: 理解 C# 中的各类指针