C#의 함수 포인터(Function Pointer)는 C# 9.0부터 도입된 기능으로, 고성능 연산이나 네이티브(C++, Delphi) 라이브러리와의 연동을 위해 만들어진 가장 로우레벨한 함수 호출 방식이다.

C++의 void (*func)(int)와 동일하게 함수의 주솟값을 직접(참조) 가리킨다. 메모리 주소를 직접 다루기 때문에 반드시 unsafe 키워드가 필요하다. csproj에서 AllowUnsafeBlocktrue로 설정한다.

Delegate나 Action 같은 객체를 생성하지 않으므로, 호출 시 가비지 컬렉터(GC) 오버헤드가 없고 CPU가 즉시 해당 주소로 점프하므로 제로 오버헤드로 볼 수 있다.

함수 포인터의 기본 문법은 delegate* 키워드를 사용하여 선언하며, 마지막 파라미터가 리턴 타입이 된다.

// 선언: <매개변수1, 매개변수2, 리턴타입>
delegate*<int, int, int> addPtr;

// 할당: & 연산자로 정적(static) 함수의 주소를 가져옴
addPtr = &MyStaticFunction;
  
// 호출: 일반 함수처럼 호출
int result = addPtr(10, 20);
// Program.cs
using System;  
  
namespace ConsoleApp;  
  
internal class Program  
{  
    private static void Main()  
    {  
        MathEngine math = new();  
        double resulMath = math.Execute(OpType.Divide, 3, 0);  
  
        if (double.IsNaN(resulMath))  
        {  
            Console.WriteLine($"Error: {resulMath}");  
        }  
        else  
        {  
            Console.WriteLine(resulMath);  
        }  
    }  
}
MathEngine.cs (Function Pointer)
namespace ConsoleApp;  
  
public unsafe class MathEngine  
{  
    private readonly delegate*<double, double, double>[] mOperationTable;
  
    public MathEngine()  
    {  
        mOperationTable = new delegate*<double, double, double>[4];  
        mOperationTable[(int)OpType.Add] = &Add;  
        mOperationTable[(int)OpType.Subtract] = &Subtract;  
        mOperationTable[(int)OpType.Multiply] = &Multiply;  
        mOperationTable[(int)OpType.Divide] = &Divide;  
    }  
  
    private static double Add(double a, double b) => a + b;  
  
    private static double Subtract(double a, double b) => a - b;  
  
    private static double Multiply(double a, double b) => a * b;  
  
    private static double Divide(double a, double b) => b != 0 ? a / b : double.NaN;
  
    public double Execute(OpType op, double a, double b)  
    {  
        int opIndex = (int)op;  
  
        if ((uint)opIndex >= (uint)mOperationTable.Length)  
        {  
            return 0;  
        }  
  
        delegate*<double, double, double> func = mOperationTable[opIndex];
        return func != null ? func(a, b) : 0;

        // if (func != null)
        // {
        //     return mOperationTable[opIndex](a, b);
        // }
        //
        // return 0;
    }  
}  
  
public enum OpType  
{  
    Add = 0,  
    Subtract = 1,  
    Multiply = 2,  
    Divide = 3  
}
Dictionary vs. Function Pointer
using System;  
using System.Collections.Generic;  
  
public class DictionaryExample  
{  
    private readonly Dictionary<string, Action<string>> mCommandMap = new();
  
    public DictionaryExample()  
    {  
        mCommandMap.Add("Print", (msg) => Console.WriteLine($"[Print] {msg}"));
        mCommandMap.Add("Log", ShowLog);  
    }  
  
    private static void ShowLog(string message)  
    {  
        Console.WriteLine($"[Log] {DateTime.Now}: {message}");  
    }  
  
    public void Execute(string commandName, string data)  
    {  
        if (mCommandMap.TryGetValue(commandName, out var action))  
        {  
            action(data);  
        }  
        else  
        {  
            Console.WriteLine("명령어를 찾을 수 없습니다.");  
        }  
    }  
}
using System;  
  
public unsafe class FunctionPointerExample  
{  
    private readonly delegate*<int, void>[] mFunctionTable;  
  
    public FunctionPointerExample()  
    {  
        mFunctionTable = new delegate*<int, void>[2];  
        mFunctionTable[0] = &DoubleValue;  
        mFunctionTable[1] = &SquareValue;  
    }  
  
    private static void DoubleValue(int x) => Console.WriteLine($"Double: {x * 2}");
  
    private static void SquareValue(int x) => Console.WriteLine($"Square: {x * x}");
  
    public void Execute(int index, int value)  
    {  
        if (index >= 0 && index < mFunctionTable.Length)  
        {  
            mFunctionTable[index](value);  
        }  
    }  
}

Dictionary는 Execute("Print", "Hello")와 같이 이름으로 호출하므로 코드가 직관적이지만 내부적으로 문자열 비교와 해시 계산이 발생한다. Function Pointer는 Execute(0, 10)와 같이 인덱스로 호출하므로 매우 빠르다.