Возврат ссылки на структуру из метода, объявленную в этом самом методе, является одним из самых классических примеров "висячих ссылок". Но что если возвращается не ссылка, а структура, содержащая ссылку? И не явно, а через вызов другого метода? Как понять, где у нас явный "провис ссылки", а где нормальный код? Звучит как какая то "дичь", но подобный кейс - вполне реальная боль для авторов языков программирования.
Давайте посмотрим на примере Rust и C# как авторы решают эту неоднозначную проблему.
Постановка задачи
Как понять в compile time, что ссылка не выходит за scope метода, в случае вызова метода, в который передается ссылка на объект, и возвращается структура, содержащая ссылку как поле, и данная структура выходит за контекст обозначенного метода?
Звучит мудрено, лучше посмотреть по коду:
pub struct MyStruct<'a> {
reference: &'a i32,
}
pub fn test<'a>() -> MyStruct<'a> {
let integer = 42;
return create(&integer);
}
ref struct MyStruct
{
public MyStruct(ref int reference)
{
Reference = ref reference;
}
public ref int Reference;
}
MyStruct Test()
{
var integer = 42;
return Create(ref integer);
}
Проблема данного кода заключается в методе Create. Он может как приводить к "висячей ссылке":
fn create(reference: &i32) -> MyStruct {
return MyStruct {
reference
};
}
MyStruct Create(ref int reference)
{
return new MyStruct(ref reference);
}
Так и возвращать вполне адекватное значение
fn create(reference: &i32) -> MyStruct {
println!("{}", reference);
return MyStruct {
reference: Box::leak(Box::new(42))
};
}
MyStruct Create(ref int reference)
{
Console.WriteLine(reference);
var arr = new[] {42};
return new MyStruct(ref arr[0]);
}
(лайвтаймы для примеров из Rust опущены, чтобы не спойлерить)
И как же компилятору "понять", когда метод Create "хороший", а когда "плохой"? В примерах выше компилятор выдает ошибки при вызове метода Create, что есть false negative, ведь данный метод может быть как "плохим", так и "хорошим".
C# Way
Что делать разработчикам языка, если у них есть мощный компилятор и сообщество, открытое к приключениям? Конечно, вводить новые ключевые слова!
Представляю, scoped!
Копипастить документацию я не буду (детали тут https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/low-level-struct-improvements), лучше скажу в двух словах
Когда вы помечаете ref параметр как scoped, метод будет "знать", что ref будет ссылаться на значение, которое "живет" только в вызывающем методе
Другими словами, когда метод принимает ref, компилятору не сильно важно, откуда она пришла, runtime разберется (вот тут много деталей https://tooslowexception.com/managed-pointers-in-net/).
А вот если указать scoped, то на этапе компиляции будут применятся ограничения, не позволяющие "провиснуть" ссылкам. Например следующий код
MyStruct Create(scoped ref int reference)
{
return new MyStruct(ref reference);
}
Вернет ошибку [CS9075] Cannot return a parameter by reference 'reference' because it is scoped to the current method.
И это то что нам нужно - указав для метода Create параметр reference как scoped мы переносим ошибку с вызова метода в его реализацию. При этом "хороший" метод Create компилируется без ошибок:
MyStruct Create(scoped ref int reference)
{
Console.WriteLine(reference);
var arr = new[] {42};
return new MyStruct(ref arr[0]);
}
Rust Way
Риторический вопрос: если в документации по C# упоминаются lifetimes, то откуда взята эта фича? Я думаю, Вы поняли, как эта проблема решается в Rust: просто добавь lifetime!
fn create<'a, 'b>(reference: &'b i32) -> MyStruct<'a> {
return MyStruct {
reference
};
}
Обратите внимание, мы объявили два lifetimes: для входа и для выхода. И они не пересекаются! Таким образом функция create "знает", что ссылка reference живет в другом контексте, нежели возвращаемое значение.
При этом "хорошая" версия create отлично компилируется, так как там именно что lifetimes различаются:
fn create<'a, 'b>(reference: &'b i32) -> MyStruct<'a> {
println!("{}", reference);
return MyStruct {
reference: Box::leak(Box::new(42))
};
}
Заключение
Казалось бы, C# и Rust в одном предложении не должны встречаться, но это не так. Модель работы со структурами и ссылками достаточно похожи. Остальные примеры Вы можете посмотреть в видео-подкасте по ссылке ниже:
Комментарии (16)
hrls
00.00.0000 00:00+2Мне одному кажется, что примеры на rust компилируются только потому, что реальный аргумент-ссылка не используется в возвращаемом значении?
KAW Автор
00.00.0000 00:00+3В этом и смысл: в обоих случаях "хороший" метод Create не использует аргумент-ссылку в возвращаемом значении.
Но компиляторы rust и c# "не залезают" в методы при вызове. Так что понять, будет ли в возвращаемом значении ссылка, которую мы передаем, можно только по сигнатуре метода.
И вот как модифицируется сигнатура метода, чтобы избежать false negative при вызове, описано в статье :)
hrls
00.00.0000 00:00+3Не очень понятно что вы имеете ввиду под «не залезают». В rust borrow checker проверит все lifetime и их сходимость в теле функции.
Есть правило lifetime elision, по которому если пропущен явный lifetime у аргумента и возвращаемого значения, то подразумевается, что он один и равен аргументу. Вы просто отвязали аргумент-ссылку от возвращаемой структуры, как того и требует сигнатура.
KAW Автор
00.00.0000 00:00+1Я к тому что при вызове метода в compile time учитывается только сигнатура метода: параметры, возвращаемое значение, lifetimes (в том чисте заэлиженые), обобщения.
То что происходит внутри метода вызывающий код не волнует.
AgentFire
00.00.0000 00:00Ну логично, ведь изза какого-нибудь runtime условия переменную могут как подменить, так и нет. Вам же не нужна потенциальная runtime ошибка, когда её можно подсветить уже на этапе компиляции?)
vpkopylov
00.00.0000 00:00Начиная с net7/c# 11 "плохой" пример из статьи с висячей ссылкой не скомпилируется https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/breaking-changes/compiler breaking changes - dotnet 7
KAW Автор
00.00.0000 00:00И в этом как раз смысл этой фичи - вместо того, чтобы не скомпилироваться при вызове метода Create, он не скомпилируется в самом методе
vpkopylov
00.00.0000 00:00И все же это существенно так как вы пишите про способы решения проблем с висящими ссылками, ведь добавлен новый способ это делать, причем работающий автоматически без модификации кода
KAW Автор
00.00.0000 00:00Но иногда он дает false negative результат. И в статье расписал как это решается
DancingOnWater
00.00.0000 00:00Не проверял в живую, но очень странно. что первый пример на Rust скомпилировался. Метод create не накладывает ограничение на время жизни, т.е. на выходе из него должна быть MyStruct<'static>, не?
kefirr
00.00.0000 00:00+2Когда впервые попробовал `ref struct` в деле, сразу Rust вспомнился. Отличная статья, спасибо. Помогла осознать
scoped
.
Gargoni
И чего? Про что статья? Про то что Rust позволяет сделать утечьку памяти?
qqrm
Memory leaks are memory safe
vibornoff
trollface.jpg
DirectoriX
Во-первых, языки со сборкой мусора на практике тоже подвержены утечкам.
Во-вторых, функция имеет говорящее название
leak
, и в её описании можно прочитать следующее (обратите внимание на 1-е и 2-е предложения):В третьих, строго говоря, утечка памяти — только лишь подвид проблем с исчерпанием ресурса. Можно без особого труда забить диск на 100%, хоть это и сложнее сделать случайно (впрочем, нагрузочные тесты в сочетании с обильным логированием делают это на раз-два).
В-четвёртых — это, пожалуй, единственный способ вернуть ссылку на не-статический объект, при этом не используя unsafe и не ссылаясь (хех) на аргументы функции.
Ну и в-пятых — это же наколеночный пример. На практике никто не будет
leak
ать память почём зря, тем более чтоб вернуть тип, которыйимплементирует Copy
.Pastoral
Статья про мотивы разработчиков компиляторов. Про причины того, что мы имеем то, что имеем. Про то, как продажа ещё не доделанного добралась до программирования. Про то, как деньги правят Миром и ведут его к гибели, если угодно.