Наверняка каждый, кто писал на C#, сталкивался с использованием параметров out. Кажется, с ними все предельно просто и понятно.
Но так ли это на самом деле? Для начала я предлагаю начать с задачи самотестирования.
Позвольте мне напомнить вам, что вне -параметры должны быть инициализированы вызываемым методом перед выходом из него.
Теперь посмотрите на следующий фрагмент кода и проверьте, скомпилируется ли он.
МояСтрукт - какой-то значимый тип:void CheckYourself(out MyStruct obj) { // Do nothing }
public struct MyStruct
{ .
}
Если вы уверенно ответили «да» или «нет», приглашаю вас читать дальше, поскольку не все так однозначно…
Фон
Начнем с небольшой предыстории.Как мы вообще пришли к учебе? вне -параметры? Все началось с разработки еще одного диагностического правила для PVS-Студия .
Идея диагностики заключается в следующем: один из параметров метода имеет тип Токен отмены .
В этом случае этот параметр не используется в теле метода.
В результате программа может не реагировать (или реагировать несвоевременно) на некоторые действия по отмене, например, отмену операции со стороны пользователя.
Просматривая сигнализации одной из первых версий диагностики, мы обнаружили код, который выглядел примерно так: void Foo(out CancellationToken ct, .
) { .
if (flag) ct = someValue; else ct = otherValue; .
}
Очевидно, это было ложное срабатывание, поэтому я попросил коллегу добавить в набор модульных тестов еще один, «без параметров».
Он добавил тесты, в том числе такой: void TestN(out CancellationToken ct)
{
Console.WriteLine(".
");
}
В первую очередь меня интересовали тесты с инициализацией параметров, но я присмотрелся к этому.
И тут меня осенило! Как на самом деле компилируется этот код? И компилируется ли он вообще? Код скомпилирован.
Потом я понял, что статья планируется.
:) Ради эксперимента мы решили изменить Токен отмены в какой-либо другой тип значения.
Например, Промежуток времени : void TestN(out TimeSpan timeSpan)
{
Console.WriteLine(".
");
}
Не компилируется.
Ну, как и ожидалось.
Но почему пример компилируется с Токен отмены ?
модификатор выходного параметра
Давайте ещё раз вспомним, что это за модификатор параметра — вне .Вот основные моменты, взятые с docs.microsoft.com ( модификатор выходного параметра ):
- вне аргументы ключевого слова вызывают передачу по ссылке;
- Переменные передаются как вне аргументы не нужно инициализировать перед передачей при вызове метода.
Однако вызываемый метод должен присвоить значение перед возвратом метода.
Внимание – это вопрос.
В чем разница между следующими тремя методами и почему последний компилируется, а первый и второй — нет? void Method1(out String obj) // compilation error
{ }
void Method2(out TimeSpan obj) // compilation error
{ }
void Method3(out CancellationToken obj) // no compilation error
{ }
Пока никакой закономерности не видно.
Может быть, есть какие-то исключения, описанные в документации? Для типа Токен отмены , Например.
Хотя это было бы немного странно – что в этом такого особенного? В документации выше я не нашел никакой информации об этом.
Дополнительную информацию см.
в спецификации языка: Для получения дополнительной информации см.
Спецификация языка является исчерпывающим источником синтаксиса и его использования C#.
Что ж, давайте посмотрим на характеристики.
Нас интересует раздел " Выходные параметры ".
Ничего нового - все то же самое: Каждый выходной параметр метода должен быть определенно назначен перед возвратом метода.
Ну а поскольку официальная документация и спецификация языка ответов нам не дали, придется покопаться в компиляторе немного глубже.
:)
Давайте погрузимся в Рослин
Исходники Roslyn можно скачать с сайта страницы проекта на GitHub .Для экспериментов я взял ветку владелец .
Мы будем работать над решением Компиляторы.
sln .
В качестве стартового проекта для экспериментов мы используем csc.csproj .
Вы даже можете запустить его в файле с помощью наших тестов, чтобы убедиться в воспроизводимости проблемы.
Для экспериментов возьмем следующий код: struct MyStruct
{
String _field;
}
void CheckYourself(out MyStruct obj)
{
// Do nothing
}
Чтобы проверить наличие ошибки, давайте соберем и запустим компилятор для файла, содержащего этот код. И действительно, есть ошибка: ошибка CS0177: выходной параметр «obj» должен быть назначен до того, как управление покинет текущий метод. Кстати, этот пост может стать хорошей отправной точкой для погружения в код. Сам код ошибки (CS0177), вероятно, генерируется динамически, но строка формата сообщения, скорее всего, находится где-то в ресурсах.
И это правда - находим ресурс ERR_ParamUnassigned : <data name="ERR_ParamUnassigned" xml:space="preserve">
<value>The out parameter '{0}' must be assigned to
before control leaves the current method</value>
</data>
По тому же имени находим код ошибки — ERR_ParamUnassigned = 177 , а также несколько мест использования в коде.
Нас интересует место добавления ошибки (метод DefiniteAssignmentPass.ReportUnassignedOutParameter ): protected virtual void ReportUnassignedOutParameter(
ParameterSymbol parameter,
SyntaxNode node,
Location location)
{
.
bool reported = false; if (parameter.IsThis) { .
}
if (!reported)
{
Debug.Assert(!parameter.IsThis);
Diagnostics.Add(ErrorCode.ERR_ParamUnassigned, // <=
location,
parameter.Name);
}
}
Что ж, оно очень похоже на то место, которое нас интересует! Ставим точку останова и убеждаемся, что это именно то место, которое нам нужно.
По результатам в Диагностика Сообщение, которое мы увидели, будет записано:
Ну вот и отлично.
Теперь давайте изменим МояСтрукт на Токен отмены , iii. Мы еще проходим эту ветку выполнения кода, в которой пишется ошибка Диагностика .
То есть она все еще там! Какой поворот. Поэтому мало отследить место, где добавляется ошибка компиляции — нужно следить дальше.
Немного покопавшись в коде, мы придумали метод DefiniteAssignmentPass.Analyze , который инициировал запуск анализа, проверив, среди прочего, что вне -параметры инициализируются.
В нем мы обнаруживаем, что соответствующий анализ выполняется 2 раза: // Run the strongest version of analysis
DiagnosticBag strictDiagnostics = analyze(strictAnalysis: true);
.
// Also run the compat (weaker) version of analysis to see
if we get the same diagnostics.
// If any are missing, the extra ones from the strong analysis
will be downgraded to a warning.
DiagnosticBag compatDiagnostics = analyze(strictAnalysis: false);
А чуть ниже есть интересное условие: // If the compat diagnostics did not overflow and we have the same
number of diagnostics, we just report the stricter set.
// It is OK if the strict analysis had an overflow here,
causing the sets to be incomparable: the reported diagnostics will
// include the error reporting that fact.
if (strictDiagnostics.Count == compatDiagnostics.Count)
{
diagnostics.AddRangeAndFree(strictDiagnostics);
compatDiagnostics.Free();
return;
}
Ситуация постепенно проясняется.
В результате строгого и совместимого анализа, когда мы пытаемся скомпилировать наш код с помощью МояСтрукт , получается, что диагностик столько же, сколько мы выдадим в результате.
Если мы изменим в нашем примере МояСтрукт на Токен отмены , строгая диагностика будет содержать 1 ошибку (как мы уже видели), а в compatДиагностика ничего не будет.
Как следствие, вышеуказанное условие не выполняется и выполнение метода не прерывается.
Куда уходит ошибка компиляции? И это сводится к предупреждению: HashSet<Diagnostic> compatDiagnosticSet
= new HashSet<Diagnostic>(compatDiagnostics.AsEnumerable(),
SameDiagnosticComparer.Instance);
compatDiagnostics.Free();
foreach (var diagnostic in strictDiagnostics.AsEnumerable())
{
// If it is a warning (e.g. WRN_AsyncLacksAwaits),
or an error that would be reported by the compatible analysis,
just report it.
if ( diagnostic.Severity != DiagnosticSeverity.Error
|| compatDiagnosticSet.Contains(diagnostic))
{
diagnostics.Add(diagnostic);
continue;
}
// Otherwise downgrade the error to a warning.
ErrorCode oldCode = (ErrorCode)diagnostic.Code;
ErrorCode newCode = oldCode switch
{
#pragma warning disable format
ErrorCode.ERR_UnassignedThisAutoProperty
=> ErrorCode.WRN_UnassignedThisAutoProperty,
ErrorCode.ERR_UnassignedThis
=> ErrorCode.WRN_UnassignedThis,
ErrorCode.ERR_ParamUnassigned // <=
=> ErrorCode.WRN_ParamUnassigned,
ErrorCode.ERR_UseDefViolationProperty
=> ErrorCode.WRN_UseDefViolationProperty,
ErrorCode.ERR_UseDefViolationField
=> ErrorCode.WRN_UseDefViolationField,
ErrorCode.ERR_UseDefViolationThis
=> ErrorCode.WRN_UseDefViolationThis,
ErrorCode.ERR_UseDefViolationOut
=> ErrorCode.WRN_UseDefViolationOut,
ErrorCode.ERR_UseDefViolation
=> ErrorCode.WRN_UseDefViolation,
_ => oldCode, // rare but possible, e.g.
ErrorCode.ERR_InsufficientStack occurring in
strict mode only due to needing extra frames
#pragma warning restore format
};
.
var args
= diagnostic is DiagnosticWithInfo {
Info: { Arguments: var arguments }
}
? arguments
: diagnostic.Arguments.ToArray();
diagnostics.Add(newCode, diagnostic.Location, args);
}
Что происходит здесь в нашем случае при использовании Токен отмены ? Петля проходит строгая диагностика (Напоминаю, что возникает ошибка по поводу неинициализации вне -параметр).
Затем -операторский филиал если не выполняется, поскольку диагностика.
Тяжесть имеет значение DiagnosticSeverity.Error и коллекция compatDiagnosticSet пустой.
А затем код ошибки компиляции сопоставляется с новым кодом — уже предупреждениями, после чего это предупреждение генерируется и записывается в результирующую коллекцию.
Вот так ошибка компиляции превратилась в предупреждение.
:) Кстати, он имеет достаточно низкий уровень, поэтому при запуске компилятора это предупреждение может быть не видно, если не установлен флаг выдачи предупреждений на соответствующем уровне.
Мы запустили компилятор, указав дополнительный флаг: csc.exe %pathToFile% -w:5 И видим ожидаемое предупреждение:
Теперь мы разобрались, куда пропадает ошибка компиляции — она заменяется предупреждением с низким приоритетом.
Однако у нас до сих пор нет ответа на вопрос, в чем особенность Токен отмены и его отличие от МояСтрукт ? Почему при анализе метода с вне -параметр МояСтрукт Анализ совместимости обнаруживает ошибку, и когда тип параметра Токен отмены - ошибка не обнаружена? Здесь предлагаю заварить чашечку чая или кофе, так как дальше нас ждет более глубокое погружение.
Надеюсь, вы воспользовались советом и подготовились.
Мы продолжаем.
:) Помните метод ОтчетУненазначенныйПараметр , в котором была записана ошибка компиляции? Давайте поднимемся немного выше и посмотрим на вызывающий метод: protected override void LeaveParameter(ParameterSymbol parameter,
SyntaxNode syntax,
Location location)
{
if (parameter.RefKind != RefKind.None)
{
var slot = VariableSlot(parameter);
if (slot > 0 && !this.State.IsAssigned(slot))
{
ReportUnassignedOutParameter(parameter, syntax, location);
}
NoteRead(parameter);
}
}
Отличие при выполнении этих методов от строгого и совместимого анализа состоит в том, что в первом случае переменная слот имеет значение 1, а во втором - -1. Следовательно, во втором случае оно не выполняется.
затем -операторский филиал если .
Теперь нам нужно выяснить, почему во втором случае слот имеет значение -1. Давайте посмотрим на метод LocalDataFlowPass.VariableSlot : protected int VariableSlot(Symbol symbol, int containingSlot = 0)
{
containingSlot = DescendThroughTupleRestFields(
ref symbol,
containingSlot,
forceContainingSlotsToExist: false);
int slot;
return
(_variableSlot.TryGetValue(new VariableIdentifier(symbol,
containingSlot),
out slot))
? slot
: -1;
}
В нашем случае _variableSlot не содержит слота для вне -параметр соответственно _variableSlot.TryGetValue(.
) возвращает значение ЛОЖЬ , код выполняется по альтернативной ветви оператора ?:, и из метода возвращается значение -1. Теперь нам нужно понять, почему _variableSlot не содержит вне -параметр.
Немного покопавшись, мы находим метод LocalDataFlowPass.GetOrCreateSlot .
Это выглядит так: protected virtual int GetOrCreateSlot(
Symbol symbol,
int containingSlot = 0,
bool forceSlotEvenIfEmpty = false,
bool createIfMissing = true)
{
Debug.Assert(containingSlot >= 0);
Debug.Assert(symbol != null);
if (symbol.Kind == SymbolKind.RangeVariable) return -1;
containingSlot
= DescendThroughTupleRestFields(
ref symbol,
containingSlot,
forceContainingSlotsToExist: true);
if (containingSlot < 0)
{
// Error case. Diagnostics should already have been produced.
return -1;
}
VariableIdentifier identifier
= new VariableIdentifier(symbol, containingSlot);
int slot;
// Since analysis may proceed in multiple passes,
it is possible the slot is already assigned.
if (!_variableSlot.TryGetValue(identifier, out slot))
{
if (!createIfMissing)
{
return -1;
}
var variableType = symbol.GetTypeOrReturnType().
Type;
if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType))
{
return -1;
}
if ( _maxSlotDepth > 0
&& GetSlotDepth(containingSlot) >= _maxSlotDepth)
{
return -1;
}
slot = nextVariableSlot++;
_variableSlot.Add(identifier, slot);
if (slot >= variableBySlot.Length)
{
Array.Resize(ref this.variableBySlot, slot * 2);
}
variableBySlot[slot] = identifier;
}
if (IsConditionalState)
{
Normalize(ref this.StateWhenTrue);
Normalize(ref this.StateWhenFalse);
}
else
{
Normalize(ref this.State);
}
return slot;
}
Из метода понятно, что существует ряд условий, когда метод вернет значение -1 и слот не будет добавлен в _variableSlot .
Если слота для переменной еще нет и все проверки проходят успешно, то запись производится в _variableSlot : _variableSlot.Add(идентификатор, слот) .
Отлаживаем код и видим, что при выполнении строгого анализа все проверки проходят успешно, но при выполнении совместимого анализа завершаем выполнение метода в следующем операторе если : var variableType = symbol.GetTypeOrReturnType().
Type;
if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType))
{
return -1;
}
Значение переменной ForceSlotEvenIfEmpty в обоих случаях одинаково( ЛОЖЬ ), а разница в том, какое значение возвращает метод IsEmptyStructType : для строгого анализа - ЛОЖЬ , для сравнительного анализа - истинный .
Тут сразу возникают новые вопросы и желание экспериментировать.
То есть получается, что если тип вне -параметр — «пустая структура» (позже мы поймём, что это значит), компилятор считает такой код валидным и не выдаёт ошибку? В нашем примере мы удаляем из МояСтрукт поле и скомпилируйте.
struct MyStruct
{ }
void CheckYourself(out MyStruct obj)
{
// Do nothing
}
И этот код успешно компилируется! Интересно.
Я не помню упоминаний о таких возможностях в документации и спецификациях.
:) Но тогда возникает другой вопрос: как работает код в случае, когда тип вне -параметр — Токен отмены ? Ведь это явно не «пустая структура» — если посмотреть код на referencesource.microsoft.com ( ссылка на CancellationToken ), становится понятно, что этот тип содержит и методы, и свойства, и поля.
Непонятно, копнем дальше.
Мы остановились на методе LocalDataFlowPass.IsEmptyStructType : protected virtual bool IsEmptyStructType(TypeSymbol type)
{
return _emptyStructTypeCache.IsEmptyStructType(type);
}
Давайте поглубже( EmptyStructTypeCache.IsEmptyStructType ): public virtual bool IsEmptyStructType(TypeSymbol type)
{
return IsEmptyStructType(type, ConsList<NamedTypeSymbol>.
Empty);
}
И еще глубже: private bool IsEmptyStructType(
TypeSymbol type,
ConsList<NamedTypeSymbol> typesWithMembersOfThisType)
{
var nts = type as NamedTypeSymbol;
if ((object)nts == null || !IsTrackableStructType(nts))
{
return false;
}
// Consult the cache.
bool result;
if (Cache.TryGetValue(nts, out result))
{
return result;
}
result = CheckStruct(typesWithMembersOfThisType, nts);
Debug.Assert(!Cache.ContainsKey(nts) || Cache[nts] == result);
Cache[nts] = result;
return result;
}
Выполнение кода происходит посредством вызова метода EmptyStructTypeCache.CheckStruct : private bool CheckStruct(
ConsList<NamedTypeSymbol> typesWithMembersOfThisType,
NamedTypeSymbol nts)
{
.
if (!typesWithMembersOfThisType.ContainsReference(nts)) { .
typesWithMembersOfThisType
= new ConsList<NamedTypeSymbol>(nts,
typesWithMembersOfThisType);
return CheckStructInstanceFields(typesWithMembersOfThisType, nts);
}
return true;
}
Здесь в игру вступает исполнение затем -операторский филиал если , потому что коллекция типысчленсофистипип пусто (см.
метод EmptyStructTypeCache.IsEmptyStructType , где он начинает передаваться в качестве аргумента).
Уже начинает вырисовываться какая-то картина – теперь становится понятно, что такое «пустая структура».
Судя по названиям методов, это структура, не содержащая полей экземпляра.
Но напоминаю вам, что в Токен отмены Есть поля экземпляров.
Итак, давайте углубимся в метод EmptyStructTypeCache.CheckStructInstanceFields .
private bool CheckStructInstanceFields(
ConsList<NamedTypeSymbol> typesWithMembersOfThisType,
NamedTypeSymbol type)
{
.
foreach (var member in type.OriginalDefinition .
GetMembersUnordered())
{
if (member.IsStatic)
{
continue;
}
var field = GetActualField(member, type);
if ((object)field != null)
{
var actualFieldType = field.Type;
if (!IsEmptyStructType(actualFieldType,
typesWithMembersOfThisType))
{
return false;
}
}
}
return true;
}
Метод перебирает элементы экземпляра, создавая для каждого элемента «actualField».
Далее, если вам удалось получить это значение ( поле - Нет нулевой ) снова производится проверка: является ли тип этого поля «пустой структурой»? Соответственно, если найдена хотя бы одна «непустая структура», исходный тип также считается «непустой структурой».
Если все поля экземпляра являются «пустыми структурами», то исходный тип также считается «пустой структурой».
Нам придется пойти немного глубже.
Не волнуйтесь, скоро наше погружение закончится и мы расставим все точки над i. :) Давайте посмотрим на метод EmptyStructTypeCache.GetActualField : private FieldSymbol GetActualField(Symbol member, NamedTypeSymbol type)
{
switch (member.Kind)
{
case SymbolKind.Field:
var field = (FieldSymbol)member;
.
if (field.IsVirtualTupleField)
{
return null;
}
return (field.IsFixedSizeBuffer ||
ShouldIgnoreStructField(field, field.Type))
? null
: field.AsMember(type);
case SymbolKind.Event:
var eventSymbol = (EventSymbol)member;
return (!eventSymbol.HasAssociatedField ||
ShouldIgnoreStructField(eventSymbol, eventSymbol.Type))
? null
: eventSymbol.AssociatedField.AsMember(type);
}
return null;
}
Соответственно, для типа Токен отмены мы заинтересованы в случай -ветвь СимволКинд.Поле .
Мы можем понять это, только проанализировав пенис.
m_source этого типа (поскольку тип Токен отмены содержит только одно поле экземпляра - m_source ).
Рассмотрим, как происходят вычисления в этом случай -ветви в нашем случае.
поле.
IsVirtualTupleField — ЛОЖЬ .
Перейдем к условному оператору и разберем условное выражение поле.
IsFixedSizeBuffer || ДолжноIgnoreStructField(поле, поле.
Тип) .
поле.
IsFixedSizeBuffer - не наш случай.
Ожидаемое значение ЛОЖЬ .
А вот значение, возвращаемое вызовом метода ДолжноIgnoreStructField(поле, поле.
Тип), различается для строгого и совместимого анализа (помните, мы анализируем одно и то же поле одного типа).
Давайте посмотрим на тело метода EmptyStructTypeCache.ShouldIgnoreStructField : private bool ShouldIgnoreStructField(Symbol member,
TypeSymbol memberType)
{
// when we're trying to be compatible with the native compiler, we
ignore imported fields (an added module is imported)
of reference type (but not type parameters,
looking through arrays)
that are inaccessible to our assembly.
return _dev12CompilerCompatibility &&
((object)member.ContainingAssembly != _sourceAssembly ||
member.ContainingModule.Ordinal != 0) &&
IsIgnorableType(memberType) &&
!IsAccessibleInAssembly(member, _sourceAssembly);
}
Давайте посмотрим, чем отличаются строгий и сопоставимый анализ.
Хотя, возможно, вы уже сами об этом догадались.
:) Строгий анализ: _dev12Совместимость компилятора — ЛОЖЬ , следовательно, результат всего выражения равен ЛОЖЬ .
Сравнительный анализ: значения всех подвыражений — истинный , результат всего выражения равен истинный .
Теперь скатываем цепочку, поднимаясь с самого конца.
:) Мы считаем, что при выполнении анализа совместимости следует игнорировать единственное поле экземпляра типа ОтменаИсточник — m_source .
Поэтому мы считаем, что Токен отмены — «пустая структура», поэтому слот для нее не создается и запись в кэш «пустых структур» не происходит. Поскольку слота нет, мы не обрабатываем вне -параметр и не записывать ошибку компиляции при выполнении анализа совместимости.
В результате строгий и совместимый анализ дают разные результаты, в результате чего ошибка компиляции понижается до предупреждения с низким приоритетом.
То есть это не какая-то специальная обработка типа Токен отмены — есть ряд типов, для которых отсутствие инициализации вне -параметр не приведет к ошибкам компиляции.
Попробуем на практике посмотреть, для каких типов компиляция будет успешной.
Как обычно, возьмем наш типичный метод: void CheckYourself(out MyType obj)
{
// Do nothing
}
И мы пытаемся заменить Мой тип Различные виды.
Мы уже видели, что этот код успешно компилируется для Токен отмены и для пустой структуры.
Что еще? struct MyStruct
{ }
struct MyStruct2
{
private MyStruct _field;
}
Если вместо этого Мой тип мы используем МояСтрукт2 , код также успешно компилируется.
public struct MyExternalStruct
{
private String _field;
}
При использовании этого типа код будет успешно скомпилирован, если Моя внешняя структура объявлено во внешней сборке.
Если в одной сборке с методом Проверь себя - не скомпилируется.
При использовании этого типа из внешней сборки код больше не будет компилироваться (изменён уровень доступа к полю _поле С частный на общественный ): public struct MyExternalStruct
{
public String _field;
}
При таком изменении типа код также не скомпилируется (мы изменили тип поля с Нить на интервал ): public struct MyExternalStruct
{
private int _field;
}
В общем, как вы понимаете, определенный простор для экспериментов есть.
Давайте подведем итоги
Общий, вне -Параметры должны быть инициализированы до того, как вызываемый метод вернет управление вызывающему объекту.Однако, как показывает практика, компилятор может внести коррективы в это требование, и в некоторых случаях вместо ошибки компиляции будет выдано низкоуровневое предупреждение.
Мы подробно обсуждали, почему это происходит, в предыдущем разделе.
А как насчет типов, которые не нужно инициализировать? вне -параметры? Например, инициализация параметра не требуется, если тип представляет собой структуру без полей.
Или если все поля являются структурами без полей.
Или вот случай с Токен отмены : с ним компиляция проходит успешно, так как этот тип находится во внешней библиотеке, единственное поле m_source ссылочный тип, а само поле недоступно из внешнего кода.
В принципе, нетрудно придумать и создать более похожие типы, при использовании которых можно избежать инициализации вне -параметры и успешно скомпилируйте свой код. Возвращаясь к вопросу из начала статьи: void CheckYourself(out MyStruct obj)
{
// Do nothing
}
public struct MyStruct
{ .
}
Этот код компилируется? Как вы уже поняли, ни «Да», ни «Нет» не являются правильным ответом.
В зависимости от того, что это такое МояСтрукт (какие там поля, где объявлен тип и т.д.), этот код может как компилироваться, так и не компилироваться.
Заключение
Именно это мы сегодня и сделали, углубившись в исходный код компилятора, чтобы ответить на, казалось бы, простой вопрос.Думаю, мы скоро повторим этот опыт, поскольку тема для следующей подобной статьи уже есть.
Так что оставайтесь на связи.
;) Кстати, приглашаю вас подписаться на мой аккаунт в Твиттере , где я также размещаю статьи и другие интересные находки.
Так что ничего интересного вы точно не пропустите.
:) Если вы хотите поделиться этой статьей с англоязычной аудиторией, воспользуйтесь ссылкой для перевода: Сергей Васильев.
Должны ли мы инициализировать выходной параметр перед возвратом метода? .
Теги: #C++ #.
NET #компилятор #roslyn #.
NET Compiler Platform
-
В Поисках Оптимального Средства
19 Oct, 24 -
Там Вкусно - Едим И Ценим
19 Oct, 24