Возврат ссылки на структуру из метода, объявленную в этом самом методе, является одним из самых классических примеров "висячих ссылок". Но что если возвращается не ссылка, а структура, содержащая ссылку? И не явно, а через вызов другого метода? Как понять, где у нас явный "провис ссылки", а где нормальный код? Звучит как какая то "дичь", но подобный кейс - вполне реальная боль для авторов языков программирования.

Давайте посмотрим на примере 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)


  1. Gargoni
    00.00.0000 00:00

    И чего? Про что статья? Про то что Rust позволяет сделать утечьку памяти?


    1. qqrm
      00.00.0000 00:00
      +6

      Memory leaks are memory safe


      1. vibornoff
        00.00.0000 00:00

        trollface.jpg


    1. DirectoriX
      00.00.0000 00:00
      +4

      Во-первых, языки со сборкой мусора на практике тоже подвержены утечкам.
      Во-вторых, функция имеет говорящее название leak, и в её описании можно прочитать следующее (обратите внимание на 1-е и 2-е предложения):

      This function is mainly useful for data that lives for the remainder of the program’s life. Dropping the returned reference will cause a memory leak. If this is not acceptable, the reference should first be wrapped with the Box::from_raw function producing a Box. This Box can then be dropped which will properly destroy T and release the allocated memory.

      В третьих, строго говоря, утечка памяти — только лишь подвид проблем с исчерпанием ресурса. Можно без особого труда забить диск на 100%, хоть это и сложнее сделать случайно (впрочем, нагрузочные тесты в сочетании с обильным логированием делают это на раз-два).
      В-четвёртых — это, пожалуй, единственный способ вернуть ссылку на не-статический объект, при этом не используя unsafe и не ссылаясь (хех) на аргументы функции.
      Ну и в-пятых — это же наколеночный пример. На практике никто не будет leakать память почём зря, тем более чтоб вернуть тип, который имплементирует Copy.


    1. Pastoral
      00.00.0000 00:00
      +1

      Статья про мотивы разработчиков компиляторов. Про причины того, что мы имеем то, что имеем. Про то, как продажа ещё не доделанного добралась до программирования. Про то, как деньги правят Миром и ведут его к гибели, если угодно.


  1. hrls
    00.00.0000 00:00
    +2

    Мне одному кажется, что примеры на rust компилируются только потому, что реальный аргумент-ссылка не используется в возвращаемом значении?


    1. KAW Автор
      00.00.0000 00:00
      +3

      В этом и смысл: в обоих случаях "хороший" метод Create не использует аргумент-ссылку в возвращаемом значении.

      Но компиляторы rust и c# "не залезают" в методы при вызове. Так что понять, будет ли в возвращаемом значении ссылка, которую мы передаем, можно только по сигнатуре метода.

      И вот как модифицируется сигнатура метода, чтобы избежать false negative при вызове, описано в статье :)


      1. hrls
        00.00.0000 00:00
        +3

        Не очень понятно что вы имеете ввиду под «не залезают». В rust borrow checker проверит все lifetime и их сходимость в теле функции.

        Есть правило lifetime elision, по которому если пропущен явный lifetime у аргумента и возвращаемого значения, то подразумевается, что он один и равен аргументу. Вы просто отвязали аргумент-ссылку от возвращаемой структуры, как того и требует сигнатура.


        1. KAW Автор
          00.00.0000 00:00
          +1

          Я к тому что при вызове метода в compile time учитывается только сигнатура метода: параметры, возвращаемое значение, lifetimes (в том чисте заэлиженые), обобщения.

          То что происходит внутри метода вызывающий код не волнует.


          1. AgentFire
            00.00.0000 00:00

            Ну логично, ведь изза какого-нибудь runtime условия переменную могут как подменить, так и нет. Вам же не нужна потенциальная runtime ошибка, когда её можно подсветить уже на этапе компиляции?)


  1. 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


    1. KAW Автор
      00.00.0000 00:00

      И в этом как раз смысл этой фичи - вместо того, чтобы не скомпилироваться при вызове метода Create, он не скомпилируется в самом методе


      1. vpkopylov
        00.00.0000 00:00

        И все же это существенно так как вы пишите про способы решения проблем с висящими ссылками, ведь добавлен новый способ это делать, причем работающий автоматически без модификации кода


        1. KAW Автор
          00.00.0000 00:00

          Но иногда он дает false negative результат. И в статье расписал как это решается


  1. DancingOnWater
    00.00.0000 00:00

    Не проверял в живую, но очень странно. что первый пример на Rust скомпилировался. Метод create не накладывает ограничение на время жизни, т.е. на выходе из него должна быть MyStruct<'static>, не?


  1. kefirr
    00.00.0000 00:00
    +2

    Когда впервые попробовал `ref struct` в деле, сразу Rust вспомнился. Отличная статья, спасибо. Помогла осознать scoped.