Указатели функций
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Сводка
Это предложение предоставляет языковые конструкции, предоставляющие коды опкодов IL, которые в настоящее время не могут быть доступны эффективно или вообще в C# сегодня: ldftn
и calli
. Эти коды опкодов IL могут быть важными в коде высокой производительности, и разработчикам нужен эффективный способ доступа к ним.
Мотивация
Мотивы и фон для этой функции описаны в следующей проблеме (как и потенциальная реализация функции):
Это альтернативное предложение по проектированию встроенных функций компилятора для
Подробный дизайн
Указатели функций
Язык позволяет объявлять указатели функций с помощью синтаксиса delegate*
. Полный синтаксис подробно описан в следующем разделе, но он предназначен для того, чтобы походить на синтаксис, используемый в объявлениях типов Func
и Action
.
unsafe class Example
{
void M(Action<int> a, delegate*<int, void> f)
{
a(42);
f(42);
}
}
Эти типы представлены с помощью типа указателя функции, как описано в ECMA-335. Это означает, что вызов delegate*
будет использовать calli
, где вызов delegate
будет использовать callvirt
в методе Invoke
.
Синтаксически, хотя вызов идентичен для обоих конструкций.
Определение указателей методов ECMA-335 включает соглашение о вызове в рамках сигнатуры типа (раздел 7.1).
Соглашение о вызовах по умолчанию будет managed
. Неуправляемые соглашения о вызовах можно указать, добавив ключевое слово unmanaged
после синтаксиса delegate*
, который будет использовать платформу по умолчанию для среды выполнения. Затем в скобках в ключевом слове unmanaged
можно указать определенные неуправляемые соглашения, указав любой тип, начиная с CallConv
в пространстве имен System.Runtime.CompilerServices
, убрав префикс CallConv
. Эти типы должны поступать из основной библиотеки программы, и набор допустимых сочетаний зависит от платформы.
//This method has a managed calling convention. This is the same as leaving the managed keyword off.
delegate* managed<int, int>;
// This method will be invoked using whatever the default unmanaged calling convention on the runtime
// platform is. This is platform and architecture dependent and is determined by the CLR at runtime.
delegate* unmanaged<int, int>;
// This method will be invoked using the cdecl calling convention
// Cdecl maps to System.Runtime.CompilerServices.CallConvCdecl
delegate* unmanaged[Cdecl] <int, int>;
// This method will be invoked using the stdcall calling convention, and suppresses GC transition
// Stdcall maps to System.Runtime.CompilerServices.CallConvStdcall
// SuppressGCTransition maps to System.Runtime.CompilerServices.CallConvSuppressGCTransition
delegate* unmanaged[Stdcall, SuppressGCTransition] <int, int>;
Преобразования между типами delegate*
выполняются на основе их сигнатуры, включая соглашение о вызове.
unsafe class Example {
void Conversions() {
delegate*<int, int, int> p1 = ...;
delegate* managed<int, int, int> p2 = ...;
delegate* unmanaged<int, int, int> p3 = ...;
p1 = p2; // okay p1 and p2 have compatible signatures
Console.WriteLine(p2 == p1); // True
p2 = p3; // error: calling conventions are incompatible
}
}
Тип delegate*
— это тип указателя, который означает, что он имеет все возможности и ограничения стандартного типа указателя:
- Допустимо только в контексте
unsafe
. - Методы, содержащие параметр
delegate*
или возвращаемый тип, можно вызывать только из контекстаunsafe
. - Невозможно преобразовать в
object
. - Нельзя использовать в качестве универсального аргумента.
- Может неявно преобразовать
delegate*
вvoid*
. - Может явно преобразоваться из
void*
вdelegate*
.
Ограничения:
- Пользовательские атрибуты нельзя применять к
delegate*
или его элементам. - Параметр
delegate*
нельзя пометить какparams
- Тип
delegate*
имеет все ограничения обычного типа указателя. - Арифметика указателей не может выполняться непосредственно с типами указателей на функции.
Синтаксис указателя функции
Полный синтаксис указателя функции представлен следующей грамматикой:
pointer_type
: ...
| funcptr_type
;
funcptr_type
: 'delegate' '*' calling_convention_specifier? '<' funcptr_parameter_list funcptr_return_type '>'
;
calling_convention_specifier
: 'managed'
| 'unmanaged' ('[' unmanaged_calling_convention ']')?
;
unmanaged_calling_convention
: 'Cdecl'
| 'Stdcall'
| 'Thiscall'
| 'Fastcall'
| identifier (',' identifier)*
;
funptr_parameter_list
: (funcptr_parameter ',')*
;
funcptr_parameter
: funcptr_parameter_modifier? type
;
funcptr_return_type
: funcptr_return_modifier? return_type
;
funcptr_parameter_modifier
: 'ref'
| 'out'
| 'in'
;
funcptr_return_modifier
: 'ref'
| 'ref readonly'
;
Если calling_convention_specifier
не задано, значение по умолчанию managed
. Точное кодирование метаданных calling_convention_specifier
и допустимых identifier
в unmanaged_calling_convention
рассматривается в представлении метаданных соглашений о вызове.
delegate int Func1(string s);
delegate Func1 Func2(Func1 f);
// Function pointer equivalent without calling convention
delegate*<string, int>;
delegate*<delegate*<string, int>, delegate*<string, int>>;
// Function pointer equivalent with calling convention
delegate* managed<string, int>;
delegate*<delegate* managed<string, int>, delegate*<string, int>>;
Преобразования указателя функции
В небезопасном контексте набор доступных неявных преобразований (неявные преобразования) расширен, чтобы включить следующие неявные преобразования указателя:
- существующие преобразования - (§23.5)
- От funcptr_type
F0
до другого funcptr_typeF1
, если все следующие условия выполняются:-
F0
иF1
имеют одинаковое количество параметров, а каждый параметрD0n
вF0
имеет те жеref
,out
илиin
модификаторы, что и соответствующий параметрD1n
вF1
. - Для каждого параметра значения (параметра без модификаторов
ref
,out
илиin
) существует преобразование идентичности, неявное преобразование ссылки или неявное преобразование указателя из типа параметра вF0
в соответствующий тип параметра вF1
. - Для каждого параметра
ref
,out
илиin
тип параметра вF0
совпадает с соответствующим типом параметра вF1
. - Если возвращаемый тип передается по значению (нет
ref
илиref readonly
), то существует идентификатор, неявная ссылка или неявное преобразование указателя от возвращаемого типаF1
к возвращаемому типуF0
. - Если возвращаемый тип является по ссылке (
ref
илиref readonly
), то возвращаемый тип и модификаторыref
уF1
совпадают с возвращаемым типом и модификаторамиref
уF0
. - Соглашение о вызове
F0
совпадает с соглашением о вызовеF1
.
-
Разрешить получение адреса целевых методов
Теперь группы методов будут разрешены как аргументы в выражении адреса. Тип такого выражения будет delegate*
, который имеет эквивалентную сигнатуру целевого метода и соглашение об управляемом вызове:
unsafe class Util {
public static void Log() { }
void Use() {
delegate*<void> ptr1 = &Util.Log;
// Error: type "delegate*<void>" not compatible with "delegate*<int>";
delegate*<int> ptr2 = &Util.Log;
}
}
В небезопасном контексте метод M
совместим с типом указателя функции F
, если все следующие значения имеют значение true:
-
M
иF
имеют одинаковое количество параметров, и каждый параметр вM
имеет те же модификаторыref
,out
илиin
, что и соответствующий параметр вF
. - Для каждого значимого параметра (параметр без
ref
,out
или модификатораin
) существуют тождественные преобразования, неявные преобразования ссылок или неявные преобразования указателя из типа параметра вM
в соответствующий тип параметра вF
. - Для каждого параметра
ref
,out
илиin
тип параметра вM
совпадает с соответствующим типом параметра вF
. - Если возвращаемый тип возвращается по значению (нет
ref
илиref readonly
), из возвращаемого типаF
к возвращаемому типуM
существует идентификатор, неявная ссылка или неявное преобразование указателя. - Если возвращаемый тип передаётся по ссылке (
ref
илиref readonly
), возвращаемый тип и модификаторыref
F
такие же, как тип возврата и модификаторыref
M
. - Соглашение о вызове
M
совпадает с соглашением о вызовеF
. Это включает как бит соглашения о вызовах, так и все флаги соглашений о вызовах, указанные в неуправляемом идентификаторе. -
M
является статическим методом.
В небезопасном контексте неявное преобразование существует из выражения, целевого объекта которого является группа методов E
к совместимому типу указателя функции F
, если E
содержит по крайней мере один метод, применимый в обычной форме к списку аргументов, созданному с помощью типов параметров и модификаторов F
, как описано ниже.
- Выбран один метод
M
, соответствующий вызову метода формыE(A)
со следующими изменениями:- Список аргументов
A
— это список выражений, каждый из которых классифицируется как переменная и с типом и модификатором (ref
,out
илиin
) соответствующего funcptr_parameter_listF
. - Методы-кандидаты — это только те методы, которые применимы в обычной форме, а не те, которые применимы в развернутой форме.
- Методы-кандидаты — это только те методы, которые являются статическими.
- Список аргументов
- Если алгоритм разрешения перегрузки выдает ошибку, возникает ошибка во время компиляции. В противном случае алгоритм создает один лучший метод
M
с таким же количеством параметров, что иF
, и преобразование считается существующим. - Выбранный метод
M
должен быть совместим (как определено выше) с типом указателя функцииF
. В противном случае возникает ошибка во время компиляции. - Результатом преобразования является указатель функции типа
F
.
Это означает, что разработчики могут зависеть от правил разрешения перегрузки для работы в сочетании с адресом оператора:
unsafe class Util {
public static void Log() { }
public static void Log(string p1) { }
public static void Log(int i) { }
void Use() {
delegate*<void> a1 = &Log; // Log()
delegate*<int, void> a2 = &Log; // Log(int i)
// Error: ambiguous conversion from method group Log to "void*"
void* v = &Log;
}
}
Оператор адреса будет реализован с помощью инструкции ldftn
.
Ограничения этой функции:
- Применяется только к методам, помеченным как
static
. - Не-
static
локальные функции нельзя использовать в&
. Сведения о реализации этих методов намеренно не указываются языком. Это включает в себя, являются ли они статическими или экземплярными, или с какой именно сигнатурой они создаются.
Операторы типов указателей функций
Раздел в небезопасном коде для выражений изменяется следующим образом:
В небезопасном контексте доступны несколько конструкций для работы со всеми _pointer_type_s, которые не _funcptr_type_s.
- Оператор
*
может использоваться для выполнения косвенного указания (§23.6.2).- Оператор
->
может использоваться для доступа к члену структуры через указатель (§23.6.3).- Оператор
[]
может использоваться для индексирования указателя (§23.6.4).- Оператор
&
может использоваться для получения адреса переменной (§23.6.5).- Операторы
++
и--
могут использоваться для увеличения и уменьшения указателей (§23.6.6).- Операторы
+
и-
могут использоваться для арифметики указателя (§23.6.7).- Операторы
==
,!=
,<
,>
,<=
и=>
могут использоваться для сравнения указателей (§23.6.8).- Оператор
stackalloc
может использоваться для выделения памяти из стека вызовов (§23.8).- Оператор
fixed
может использоваться для временного фиксации переменной, чтобы её адрес можно было получить (§23.7).В небезопасном контексте доступны несколько конструкций для работы со всеми _funcptr_type_s.
- Оператор
&
может использоваться для получения адреса статических методов (Разрешение получения адреса для целевых методов)- Операторы
==
,!=
,<
,>
,<=
и=>
могут использоваться для сравнения указателей (§23.6.8).
Кроме того, мы изменяем все разделы в Pointers in expressions
, чтобы запретить типы указателей функций, за исключением Pointer comparison
и The sizeof operator
.
Лучший член функции
§12.6.4.3 Лучший элемент функции будет изменен, чтобы включить следующую строку:
delegate*
более конкретно, чемvoid*
Это означает, что можно перегрузить void*
и delegate*
и по-прежнему разумно использовать адрес оператора.
Вывод типов
В небезопасном коде в алгоритмы вывода типов вносятся следующие изменения:
Типы входных данных
Добавляется следующее:
Если
E
является группой методов адреса иT
является типом указателя функции, то все типы параметровT
являются входными типамиE
с типомT
.
Типы выходных данных
Добавляется следующее:
Если
E
является группой методов адреса иT
является типом указателя функции, возвращаемый типT
является типом выходных данныхE
с типомT
.
Вывод типов выходных данных
Следующий маркер добавляется между маркерами 2 и 3.
- Если
E
является группой методов адреса иT
является типом указателя функции с типами параметровT1...Tk
и возвращаемым типомTb
, а разрешение перегрузкиE
с типамиT1..Tk
дает один метод с типом возвратаU
, то вывод нижней границы производится изU
наTb
.
Более качественное преобразование выражения
Следующий подпункт добавляется в качестве элемента в пункт 2:
V
является типом указателя функцииdelegate*<V2..Vk, V1>
иU
является типом указателя функцииdelegate*<U2..Uk, U1>
, а соглашение о вызовеV
идентичноU
, и ссылкаVi
идентичнаUi
.
Умозаключения с нижней границей
В пункт 3 добавляется следующий случай:
V
является типом указателя функцииdelegate*<V2..Vk, V1>
, и существует тип указателя функцииdelegate*<U2..Uk, U1>
такой, чтоU
идентиченdelegate*<U2..Uk, U1>
, соглашение о вызовеV
идентичноU
, а ссылочная характеристикаVi
идентичнаUi
.
Первый пункт вывода от Ui
к Vi
изменен на:
- Если
U
не является типом указателя на функцию иUi
не известно, что является ссылочным типом, или еслиU
является типом указателя на функцию иUi
не известно, что является типом указателя на функцию или ссылочным типом, то выполняется точный вывод
Затем добавьте после третьего пункта вывода от Ui
до Vi
:
- В противном случае, если
V
равноdelegate*<V2..Vk, V1>
, вывод зависит от i-го параметраdelegate*<V2..Vk, V1>
:
- Если версия 1:
- Если возвращаемое значение равно значению, выполняется вывод с нижней границой.
- Если возвращается по ссылке, выполняется точный вывод.
- Если V2..Vk:
- Если параметр имеет значение, выполняется вывод с верхней границой.
- Если параметр указан по ссылке, то выполняется точный вывод.
Выводы о верхних границах
В пункт 2 добавляется следующий случай:
U
— это тип указателя функцииdelegate*<U2..Uk, U1>
,V
— это тип указателя функции, идентичныйdelegate*<V2..Vk, V1>
, соглашение о вызове дляU
идентичноV
, а характеристика ссылки дляUi
идентичнаVi
.
Первый пункт вывода из Ui
на Vi
изменён на:
- Если
U
не является типом указателя функции иUi
неизвестно, является ли ссылочным типом, или еслиU
является типом указателя функции иUi
неизвестно, является ли типом указателя функции или ссылочным типом, то выполняется точный вывод.
Затем добавьте после 3-го пункта вывода из Ui
в Vi
:
- В противном случае, если
U
являетсяdelegate*<U2..Uk, U1>
, вывод зависит от i-го параметраdelegate*<U2..Uk, U1>
:
- Если U1:
- Если возврат происходит по значению, то выполняется вывод с верхней границей.
- Если возвращается по ссылке, выполняется точный вывод.
- Если U2..Uk:
- Если параметр имеет значение, выполняется вывод с нижней границ ой.
- Если параметр указан по ссылке, выполняется точный вывод.
Представление метаданных параметров и возвращаемых типов in
, out
и ref readonly
Подписи указателей функций не имеют места для указания признаков параметров, поэтому мы должны закодировать, являются ли параметры и тип возвращаемого значения in
, out
или ref readonly
, с помощью modreqs.
in
Мы повторно используем System.Runtime.InteropServices.InAttribute
, применяемый как modreq
к спецификатору ссылки параметра или возвращаемого типа, подразумевая следующее:
- Если применяется к спецификаторам ссылки параметра, этот параметр обрабатывается как
in
. - Если применяется к спецификатору возвращаемого типа ref, возвращаемый тип обрабатывается как
ref readonly
.
out
Мы используем System.Runtime.InteropServices.OutAttribute
, применяемый в качестве modreq
к спецификатору ссылки на тип параметра, чтобы означать, что параметр является параметром out
.
Ошибки
- Ошибка применения
OutAttribute
в качестве modreq для типа возвращаемого значения. - Ошибка заключается в применении одновременно
InAttribute
иOutAttribute
как модификаторов требования к типу параметра. - Если одно из них указано с помощью modopt, они игнорируются.
Представление метаданных соглашений о вызовах
Соглашения о вызовах кодируются в сигнатуре метода в метаданных путем сочетания флага CallKind
в сигнатуре и одного или нескольких modopt
в начале сигнатуры. В настоящее время ECMA-335 объявляет следующие элементы в флаге CallKind
:
CallKind
: default
| unmanaged cdecl
| unmanaged fastcall
| unmanaged thiscall
| unmanaged stdcall
| varargs
;
Из них указатели функций в C# поддерживают все, кроме varargs
.
Кроме того, среда выполнения (и в конечном итоге 335) будет обновлена, чтобы включить новый CallKind
на новых платформах. В настоящее время это не имеет официального имени, но этот документ будет использовать unmanaged ext
в качестве заполнителя, обозначающего новый расширяемый формат соглашений о вызовах. Без modopt
unmanaged ext
— это соглашение о вызовах платформы по умолчанию, unmanaged
без квадратных скобок.
Сопоставление calling_convention_specifier
с CallKind
calling_convention_specifier
, который опущен или указан как managed
, сопоставляется с default
CallKind
. Это значение по умолчанию CallKind
любого метода, не связанного с UnmanagedCallersOnly
.
C# распознает 4 специальных идентификаторов, которые сопоставляются с конкретными неуправляемыми CallKind
из ECMA 335. Чтобы это сопоставление выполнялось, эти идентификаторы должны быть указаны самостоятельно без других идентификаторов, и это требование закодировано в спецификацию для unmanaged_calling_convention
s. Эти идентификаторы представляют собой Cdecl
, Thiscall
, Stdcall
и Fastcall
, которые соответствуют unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
и unmanaged fastcall
соответственно. Если задано несколько identifer
или один identifier
не является специально распознаваемым идентификатором, мы выполняем специальный поиск имен для идентификатора со следующими правилами:
- Мы добавляем в начало
identifier
строкуCallConv
- Мы рассмотрим только типы, определенные в пространстве имен
System.Runtime.CompilerServices
. - Мы рассмотрим только типы, определенные в основной библиотеке приложения, которая является библиотекой, которая определяет
System.Object
и не имеет зависимостей. - Мы рассмотрим только общедоступные типы.
Если поиск выполняется успешно на всех identifier
, указанных в unmanaged_calling_convention
, кодируем CallKind
как unmanaged ext
и кодируем каждый из разрешенных типов в наборе modopt
в начале сигнатуры указателя функции. Как примечание, эти правила означают, что пользователи не могут префиксить эти identifier
с CallConv
, так как это приведет к поиску CallConvCallConvVectorCall
.
При интерпретации метаданных сначала рассмотрим CallKind
. Если это что-либо, отличное от unmanaged ext
, мы игнорируем все modopt
в возвращаемом типе для определения соглашения о вызовах и используем только CallKind
. Если CallKind
является unmanaged ext
, мы рассматриваем modopts в начале типа указателя функции, принимая объединение всех типов, которые соответствуют следующим требованиям:
- Он определен в основной библиотеке, которая не ссылается на другие библиотеки и определяет
System.Object
. - Тип определяется в пространстве имен
System.Runtime.CompilerServices
. - Тип начинается с префикса
CallConv
. - Тип является общедоступным.
Они представляют типы, которые необходимо найти при поиске identifier
в unmanaged_calling_convention
при определении типа указателя функции в источнике.
Ошибка при попытке использовать указатель функции с CallKind
на unmanaged ext
, если целевая среда выполнения не поддерживает эту возможность. Это определяется путем поиска наличия константы System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind
. Если эта константа присутствует, среда выполнения считается поддерживаемой функцией.
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
— это атрибут, используемый средой CLR, чтобы указать, что метод должен вызываться с определенным соглашением о вызовах. Из-за этого мы введем следующую поддержку работы с атрибутом:
- Ошибка заключается в прямом вызове метода, аннотированного этим атрибутом, из C#. Пользователи должны получить указатель функции на метод, а затем вызвать этот указатель.
- Это ошибка применения атрибута к чему-либо, кроме обычного статического метода или обычной локальной функции. Компилятор C# помечает любые нестатические или статические не обычные методы, импортированные из метаданных с этим атрибутом, как неподдерживаемые языком.
- Является ошибкой, если метод, помеченный атрибутом, имеет параметр или возвращаемый тип, который не является
unmanaged_type
. - Ошибкой является наличие у метода, помеченного атрибутом, параметров типа, даже если эти параметры типа ограничены
unmanaged
. - Ошибка возникает, когда метод в универсальном типе помечен атрибутом.
- Ошибка возникает при преобразовании метода, отмеченного атрибутом, в тип делегата.
- Это ошибка указывать типы для
UnmanagedCallersOnly.CallConvs
, которые не соответствуют требованиям к соглашению о вызовеmodopt
в метаданных.
При определении соглашения о вызове метода, помеченного допустимым атрибутом UnmanagedCallersOnly
, компилятор выполняет следующие проверки типов, указанных в свойстве CallConvs
, чтобы определить действующие CallKind
и modopt
, которые следует использовать для определения соглашения о вызовах:
- Если типы не указаны,
CallKind
рассматривается какunmanaged ext
, без указания соглашения о вызовахmodopt
в начале типа указателя на функцию. - Если указан один из типов, и этот тип называется
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
илиCallConvFastcall
, то элементCallKind
рассматривается какunmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
илиunmanaged fastcall
соответственно, без наличия соглашения о вызовеmodopt
в начале типа указателя функции. - Если указано несколько типов или один тип не назван одним из специально упомянутых выше,
CallKind
рассматривается какunmanaged ext
, при этом объединение указанных типов обрабатывается какmodopt
в начале типа указателя функции.
Затем компилятор смотрит на эту эффективную коллекцию CallKind
и modopt
и использует обычные правила метаданных для определения окончательной конвенции вызова типа указателя функции.
Открытые вопросы
Проверка наличия поддержки среды выполнения для unmanaged ext
https://github.com/dotnet/runtime/issues/38135 отслеживает добавление этого флага. В зависимости от отзыва по результатам проверки мы будем использовать свойство, указанное в задаче, или использовать наличие UnmanagedCallersOnlyAttribute
в качестве флага, определяющего, поддерживает ли среда выполнения unmanaged ext
.
Соображения
Разрешить методы экземпляра
Предложение может быть расширено для поддержки методов экземпляров, используя соглашение о вызове EXPLICITTHIS
CLI (именованное instance
в коде C#). Эта форма указателей функции CLI помещает параметр this
в качестве явного первого параметра синтаксиса указателя функции.
unsafe class Instance {
void Use() {
delegate* instance<Instance, string> f = &ToString;
f(this);
}
}
Это обоснованно, но усложняет предложение. Особенно потому, что указатели функций, отличающиеся соглашением о вызовах instance
и managed
, будут несовместимы, хотя оба варианта используются для вызова управляемых методов с одной и той же подписью C#. Кроме того, в каждом случае, когда было бы ценно иметь такое решение, существовала простая альтернатива: использование локальной функции static
.
unsafe class Instance {
void Use() {
static string toString(Instance i) => i.ToString();
delegate*<Instance, string> f = &toString;
f(this);
}
}
Не требуйте использования небезопасного кода в декларации
Вместо требования unsafe
при каждом использовании delegate*
, это требуется только в тот момент, когда группа методов преобразуется в delegate*
. Именно здесь возникают основные проблемы безопасности (зная, что содержащая сборка не может быть выгружена, пока значение активно). Требование unsafe
в других местах можно рассматривать как чрезмерное.
Это то, как проект был первоначально предназначен. Но полученные языковые правила выглядели очень неуклюже. Невозможно скрыть тот факт, что это значение указателя, которое продолжает проступать даже без ключевого слова unsafe
. Например, преобразование в object
не может быть разрешено, оно не может быть членом class
и т. д. Для всех использования указателей на C# требуется unsafe
, поэтому этот дизайн следует этому.
Разработчики по-прежнему смогут представить безопасную оболочку над значениями delegate*
так же, как это делается для обычных типов указателей сегодня. Рассмотрим:
unsafe struct Action {
delegate*<void> _ptr;
Action(delegate*<void> ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
Использование делегатов
Вместо использования нового синтаксического элемента delegate*
просто используйте существующие типы delegate
, добавив *
после типа.
Func<object, object, bool>* ptr = &object.ReferenceEquals;
Обработку соглашения о вызовах можно осуществить путем добавления аннотации к типам delegate
с атрибутом, указывающим значение CallingConvention
. Отсутствие атрибута будет означать управляемое соглашение о вызовах.
Кодирование этого в IL проблематично. Базовое значение должно быть представлено в виде указателя, но оно также должно:
- Укажите уникальный тип, позволяющий перегрузкам с различными типами указателей функций.
- Эквивалентны для целей OHI в пределах границ сборки.
Последняя точка особенно проблематична. Это означает, что каждая сборка, использующая Func<int>*
, должна кодировать эквивалентный тип в метаданных, даже если Func<int>*
определена в сборке, которая не контролируется.
Кроме того, любой другой тип, определенный с именем System.Func<T>
в сборке, которая не является mscorlib, должна отличаться от версии, определенной в mscorlib.
Один из вариантов, который был рассмотрен, является создание такого указателя, как mod_req(Func<int>) void*
. Тем не менее это не работает, так как mod_req
не может привязаться к TypeSpec
и, следовательно, не может быть нацелен на универсальные инстанциации.
Именованные указатели функции
Синтаксис указателя функции может быть громоздким, особенно в сложных случаях, таких как вложенные указатели функции. Вместо того чтобы разработчики каждый раз вводили сигнатуру функции, язык мог бы разрешить именованные объявления указателей функций, как это делается с помощью delegate
.
func* void Action();
unsafe class NamedExample {
void M(Action a) {
a();
}
}
Часть этой проблемы заключается в том, что базовый примитив CLI не имеет имен, поэтому это было бы исключительно C#-изобретением и потребует некоторой обработки метаданных для реализации. Это можно сделать, но потребует значительных усилий. По сути, для этих имен требуется, чтобы C# имел сопутствующую таблицу определения типов.
Кроме того, когда аргументы именованных указателей функций были рассмотрены, мы обнаружили, что они могут применяться одинаково хорошо к ряду других сценариев. Например, было бы так же удобно объявлять именованные кортежи, чтобы уменьшить потребность в вводе полной подписи во всех случаях.
(int x, int y) Point;
class NamedTupleExample {
void M(Point p) {
Console.WriteLine(p.x);
}
}
После обсуждения мы приняли решение не разрешать объявление типов delegate*
с использованием имен. Если мы обнаружим, что это необходимо на основе отзывов клиентов об использовании, мы рассмотрим решение именования, которое подходит для указателей функций, кортежей, обобщений и т. д. Скорее всего, это будет аналогично другим предложениям, таким как полная поддержка typedef
в языке.
Рекомендации по будущему
статические делегаты
Это относится к предложению разрешить объявление типов delegate
, которые могут ссылаться только на членов static
. Преимуществом таких экземпляров delegate
является то, что они не требуют выделения памяти и лучше подходят для сценариев, критичных к производительности.
Если функция указателя будет реализована, предложение static delegate
, скорее всего, будет закрыто. Предлагаемое преимущество этой функции — отсутствие выделения памяти. Однако недавние исследования показали, что это невозможно из-за выгрузки сборок. Должен быть прочный хэндл от static delegate
к методу, на который он ссылается, чтобы предотвратить выгрузку сборки.
Для поддержания каждого static delegate
экземпляра потребуется выделение нового дескриптора, что противоречит целям предложения. Существовали некоторые проекты, где выделение можно было бы свести к одному на каждое место вызова, но это было довольно сложно и не казалось стоящим усилий.
Это означает, что разработчики должны решить между следующими компромиссами:
- Безопасность перед выгрузкой сборки: это требует распределений, поэтому
delegate
уже является достаточным решением. - Отсутствие безопасности при разгрузке установки: используйте
delegate*
. Это можно упаковать вstruct
, чтобы разрешить использование вне контекстаunsafe
в остальной части кода.
C# feature specifications