Freigeben über


Локальные ссылки и возврат ссылок

«Возврат ссылок» является темой еще одного отличного вопроса на StackOverflow, которым я хочу поделиться со своей аудиторией.

Начиная с C# 1.0 можно было создавать «синонимы» (alias) переменной путем «передачи ее по ссылке» в некоторый метод:

 static void M(ref int x)
{
    x = 123;
}
...
int y = 456;
M(ref y);

Несмотря на разные имена, переменные «x» и «y» являются синонимами; они обе ссылаются на одну и ту же область памяти. При изменении x, у также изменяется, поскольку это одно и то же. По сути, «ref»-параметры позволяют передавать переменные в виде переменных, а не в виде значений. Это особенность не всегда очевидна (поскольку многие путают «ссылочные типы» – reference types – с передачей по ссылке с помощью ключевого слова «ref»), но, в целом, это достаточно понятная и часто используемая возможность.

Однако менее известным фактом является то, что система типов CLR поддерживает еще один вариант использования «ref», хотя он и не поддерживается языком C#. Система типов CLR поддерживает методы, возвращающие ссылки на переменные, а также создание локальных переменных, ссылающихся на другие переменные. Однако система типов CLR не позволяет создавать поля, ссылающиеся на другие переменные. Массивы также не могут содержать ссылки на другие переменные. Эти ограничения связаны с тем, что подобные ссылки слишком бы усложнили сборку мусора. (Я также должен заметить, что типы «управляемых ссылок на переменные» не могут преобразовываться к типу object, что не позволяет использовать их в виде аргументов типов для обобщенных типов и методов. Более подробная информация находится в Части 1 Секции 8.2.1.1 спецификации CLI под названием «Управляемые указатели и связанные с ними типы» – Managed pointers and related types).

Как несложно догадаться, вполне возможно эти возможности реализовать в языке C#. Тогда, было бы возможно следующее:

 static ref int Max(ref int x, ref int y) 
{ 
  if (x > y) 
    return ref x; 
  else 
    return ref y; 
}

А зачем это нужно? На самом деле, этот метод сильно отличается от стандартной функции «Max», возвращающей большее из двух значений. Этот метод возвращает переменную с большим значением, которая затем может быть изменена:

 int a = 123;
int b = 456; 
ref int c = ref Max(ref a, ref b); 
c += 100;
Console.WriteLine(b); // 556!

Здорово! Это значит, что методы, возвращающие ссылку, могут находиться слева от оператора присваивания, нам не понадобиться локальная переменная c:

 int a = 123;
int b = 456; 
Max(ref a, ref b) += 100;
Console.WriteLine(b); // 556!

Синтаксически, «ref» является четким указанием на то, что происходит что-то странное. Каждый раз, когда «ref» оказывается перед переменной, означает что «я выполняю некоторые операции над синонимом этой переменной». Каждый раз, когда это слово оказывается перед объявлением, означает, что «эта штука должна быть проинициализирована переменной, помеченной ref».

Я точно знаю, что создать версию языка C#, поддерживающую эти возможности, вполне реально, поскольку я уже это делал для проверки. Продвинутые разработчики (особенно те, кто переносит код с неуправляемого С++) часто просят о большем количестве возможностей языка С++ – например, возможность использования ссылок не прибегая к указателям и постоянной фиксации (pinning) памяти. Использование управляемых указателей дает эти преимущества, не ухудшая производительность сборщика мусора.

Мы довольно много думали над этой возможностью и, реализовали довольно приличный ее кусок, чтобы показать ее другим командам и собрать отзывы о ней. Однако на данный момент это исследование показало, что такая возможность не столь нужна и привлекательна, чтобы добавлять ее поддержку в популярный язык программирования. У нас есть задачи с более высоким приоритетом, при ограниченном количестве времени и ресурсов, так что в ближайшем будущем реализовывать эту возможность мы не собираемся.

Кроме того, правильная реализация этой возможности требует внесения изменения в CLR. Сейчас CLR считает методы, возвращающие управляемые указатели, корректными, но непроверяемыми (unverifiable), поскольку у нас нет механизма проверки нарушения следующих правил:

 static ref int M1(ref int x)
{
  return ref x;
}
static ref int M2()
{
  int y = 123;
  return ref M1(ref y); // Упс!
}
static int M3()
{
    ref int z = ref M2();
    return z;
}

Метод M3 возвращает локальную переменную метода M2, время жизни которой уже завершено! Можно написать детектор, проверяющий использования возвращаемых ссылок, которые не нарушают безопасность стека. И если этот детектор не сможет доказать выполнение правил безопасности времени жизни, то возвращение ссылок в этой части программы будет запрещено. Это не такая уж и большая работа для команды разработчиков, однако, это повлечет за собой серьезную нагрузку на команду тестирования, поскольку нужно убедиться, что рассмотрены все возможные условия. Это увеличит стоимость реализации такой возможности и еще раз подтверждает, что ее преимущества не перевешивают всех затрат.

Если мы когда-нибудь реализуем эту возможность, то как вы будете ее использовать? Есть ли у вас сценарий использования, который было бы сложно реализовать по-другому? Если это так, то расскажите об этом в комментариях. Чем больше у нас будет информации от реальных пользователей о том, почему им нужна та или иная возможность, тем выше шансы, что она когда-нибудь появится. Это небольшая интересная возможность и при наличии достаточного интереса, я хотел бы ее реализовать. Однако мы знаем, что ref-параметры является одной из наиболее плохо понимаемых возможностей, особенно для новичков, так что мы не хотим без необходимости добавлять в язык еще больше непонятных возможностей, до тех пор, пока не будут доказаны их серьезные преимущества.

Оригинал статьи