Aracılığıyla paylaş


Düşük DüzeyLi Yapı Geliştirmeleri

Not

Bu makale bir özellik belirtimidir. Belirtim, özelliğin tasarım belgesi olarak görev alır. Önerilen belirtim değişikliklerini ve özelliğin tasarımı ve geliştirilmesi sırasında gereken bilgileri içerir. Bu makaleler, önerilen belirtim değişiklikleri son haline getirilene ve geçerli ECMA belirtimine dahil edilene kadar yayımlanır.

Özellik belirtimi ile tamamlanan uygulama arasında bazı tutarsızlıklar olabilir. Bu farklılıklar, ilgili dil tasarım toplantısı (LDM) notlarındakaydedilir.

Özellik belirtimlerini C# dil standardına benimseme işlemi hakkında daha fazla bilgi edinmek içinbelirtimleri makalesinde bulabilirsiniz.

Şampiyonluk sorunları: https://github.com/dotnet/csharplang/issues/1147, https://github.com/dotnet/csharplang/issues/6476

Özet

Bu teklif, struct performans geliştirmeleri için birkaç farklı teklifin bir toplamıdır: ref alanları ve yaşam süresi varsayılanlarını geçersiz kılma olanağı. Amaç, alt düzey struct iyileştirmeleri için çeşitli teklifleri dikkate alarak genel bir özellik kümesi oluşturan bir tasarım yapmaktır.

Not: Bu belirtimin önceki sürümleri, Span güvenliği özellik belirtiminde kullanıma sunulan "ref-safe-to-escape" ve "safe-to-escape" terimlerini kullanıyordu. ECMA standart komitesi adlarını sırasıyla "ref-safe-context" ve "safe-context"olarak değiştirdi. Güvenli bağlamın değerleri tutarlı bir şekilde "declaration-block", "function-member" ve "caller-context" kullanacak şekilde geliştirilmiştir. Bölümcükler, bu terimler için farklı ifadeler kullanmış ve ayrıca "çağıran bağlam" için "dönüş için güvenli" ifadesini eş anlamlı olarak kullanmıştı. Bu belirtim, C# 7.3 standardındaki terimleri kullanacak şekilde güncelleştirildi.

Bu belgede özetlenen özelliklerin tümü C# 11'de uygulanmadı. C# 11 şunları içerir:

  1. ref alanları ve scoped
  2. [UnscopedRef]

Bu özellikler, gelecekteki bir C# sürümü için açık teklifler olmaya devam eder:

  1. ref alanlarından ref struct alanlarına
  2. Gün batımı kısıtlanmış türleri

Motivasyon

C# uygulamasının önceki sürümlerinde dile bir dizi düşük düzey performans özelliği eklendi: ref döndürür, ref struct, işlev işaretçileri vb. ... Bu, .NET geliştiricilerinin yüksek performanslı kod yazmasına olanak tanırken, tür ve bellek güvenliği için C# dil kurallarından yararlanmaya devam etti. Ayrıca Span<T>gibi .NET kitaplıklarında temel performans türlerinin oluşturulmasına da izin verdi.

.NET ekosistemindeki bu özellikler yaygınlaştıkça, hem iç hem de dış geliştiriciler ekosistemde kalan pürüz noktaları hakkında bize bilgi sağlamaktadır. İşlerini yapmak için hala unsafe koduna düşmeleri gereken yerler veya çalışma zamanında Span<T>gibi özel durum tiplerini gerektiren yerler.

Bugün Span<T>, çalışma zamanının etkili bir şekilde internal alanı olarak davrandığı ByReference<T> türü ref kullanılarak gerçekleştirilir. Bu, ref alanlarının avantajını sağlar, ancak dilin diğer refkullanımlarında olduğu gibi bunun için güvenlik doğrulaması sağlamaması dezavantajı da sağlar. internalolduğu için sadece dotnet/runtime bu türü kullanabilir, bu nedenle üçüncü taraflar ref alanlarına göre kendi temel öğelerini tasarlayamaz. Bu çalışma için motivasyonun bir parçası, kaldırmak ve tüm kod tabanlarında uygun alanları kullanmaktır.

Bu teklif, mevcut düşük düzey özelliklerimizi kullanarak bu sorunları çözmeyi planlıyor. Özellikle şu amaçlara hedefleniyor:

  • ref struct türlerinin ref alanları bildirmesine izin verin.
  • Çalışma zamanının C# türü sistemini kullanarak Span<T> tam olarak tanımlamasına ve ByReference<T> gibi özel durum türünü kaldırmasına izin verme
  • struct türlerinin alanlarına ref döndürmesine izin verin.
  • Çalışma zamanının, yaşam süresi varsayılanlarının sınırlamalarından kaynaklanan unsafe kullanımlarını kaldırmasına izin ver
  • fixed yönetilen ve yönetilmeyen türler için güvenli struct arabelleklerinin bildirilmesine izin ver

Ayrıntılı Tasarım

ref struct güvenlik kuralları, span güvenlik belgesinde önceki koşullar kullanılarak tanımlanır. Bu kurallar §9.7.2 C# 7 standardına eklenmiştir ve §16.4.12. Bu belgede, bu teklif sonucunda gerekli değişiklikler açıklanır. Onaylanan bir özellik olarak kabul edildikten sonra bu değişiklikler bu belgeye eklenir.

Bu tasarım tamamlandıktan sonra Span<T> tanımımız aşağıdaki gibi olacaktır:

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    // This constructor does not exist today but will be added as a part 
    // of changing Span<T> to have ref fields. It is a convenient, and
    // safe, way to create a length one span over a stack value that today 
    // requires unsafe code.
    public Span(ref T value)
    {
        _field = ref value;
        _length = 1;
    }
}

Başvuru alanlarını ve kapsamı belirlenmiş alanları sağlayın

Dil, geliştiricilerin bir refiçinde ref struct alanları bildirmesine olanak sağlar. Bu, örneğin büyük değiştirilebilir struct örneklerini kapsüllerken veya çalışma zamanının yanı sıra kitaplıklarda Span<T> gibi yüksek performanslı türler tanımlarken yararlı olabilir.

ref struct S 
{
    public ref int Value;
}

ref imzası kullanılarak meta veriler içine bir ELEMENT_TYPE_BYREF alanı gönderilir. Bu, yerel ref veya ref bağımsız değişkenlerini üretmekten farklı değildir. Örneğin ref int _fieldELEMENT_TYPE_BYREF ELEMENT_TYPE_I4olarak gönderilir. Bu, bu girişe izin vermek için ECMA335'i güncellememizi gerektirir, ancak bu işlem oldukça basit olmalıdır.

Geliştiriciler, ref struct ifadesini kullanarak bir ref alanıyla default başlatmaya devam edebilir ve bu durumda bildirilen tüm ref alanları nulldeğerine sahip olur. Bu tür alanları kullanmaya yönelik her girişim bir NullReferenceException atılmasıyla sonuçlanır.

ref struct S 
{
    public ref int Value;
}

S local = default;
local.Value.ToString(); // throws NullReferenceException

tr-TR: C# dili, bir ref'ın null olamayacağını iddia etse de, bu çalışma zamanı düzeyinde yasaldır ve iyi tanımlanmış semantiklere sahiptir. türlerine alanları tanıtan geliştiricilerin bu olasılığın farkında olması ve bu ayrıntıların kod kullanımına sızmasını kesinlikle gerekir. ref alanlar, bunun yerine çalışma zamanı yardımcıları kullanılarak null olmayan olarak doğrulanmalı ve başlatılmamış bir struct yanlış kullanıldığında hata fırlatılmalıdır.

ref struct S1 
{
    private ref int Value;

    public int GetValue()
    {
        if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
        {
            throw new InvalidOperationException(...);
        }

        return Value;
    }
}

ref alanı, readonly değiştiricilerle aşağıdaki yollarla birleştirilebilir:

  • readonly ref: Bu, oluşturucu veya init yöntemleri dışında yeniden atanamayan bir başvuru alanıdır. Bu bağlamların dışında olsa da değer atanabilir
  • ref readonly: Bu, referans yeniden atanabilir, ancak herhangi bir noktada değer atanamaz bir alandır. Bu şekilde, bir in parametresi bir ref alanına referans olarak yeniden atanabilir.
  • readonly ref readonly: ref readonly ve readonly refbirleşimi.
ref struct ReadOnlyExample
{
    ref readonly int Field1;
    readonly ref int Field2;
    readonly ref readonly int Field3;

    void Uses(int[] array)
    {
        Field1 = ref array[0];  // Okay
        Field1 = array[0];      // Error: can't assign ref readonly value (value is readonly)
        Field2 = ref array[0];  // Error: can't repoint readonly ref
        Field2 = array[0];      // Okay
        Field3 = ref array[0];  // Error: can't repoint readonly ref
        Field3 = array[0];      // Error: can't assign ref readonly value (value is readonly)
    }
}

readonly ref struct için ref alanlarının readonly refbildirilmesi gerekecektir. readonly ref readonly'nin bildirilmesi gerektiğine dair bir zorunluluk yoktur. Bu, bir readonly struct'a bu tür bir alan aracılığıyla dolaylı mutasyonlar yapabilmeye imkan tanır, ancak bu, bugün referans türüne işaret eden bir readonly alanından farklı değildir (daha fazla ayrıntı).

readonly ref, diğer tüm alanlarda olduğu gibi initonly bayrağı kullanılarak meta veriye gönderilir. bir ref readonly alanı System.Runtime.CompilerServices.IsReadOnlyAttributeile ilişkilendirilecektir. Her iki öğeyle de bir readonly ref readonly gönderilir.

Bu özellik için çalışma zamanı desteği ve ECMA belirtiminde değişiklikler yapılması gerekir. Bu nedenle bunlar yalnızca ilgili özellik bayrağı corelib içinde ayarlandığında etkinleştirilir. Tam API'yi izleme sorunu burada https://github.com/dotnet/runtime/issues/64165

ref alanlara izin vermek için gerekli olan güvenli bağlam kurallarımızda yapılan değişiklikler kümesi küçüktür ve hedeflenmiştir. Kurallar zaten API'lerden var olan ve tüketilen ref alanlarını hesaba alır. Değişikliklerin yalnızca iki yönüne odaklanması gerekir: bunların nasıl oluşturulduğu ve nasıl yeniden atandıkları.

İlk olarak, alanlar için ref-safe-context değerlerini oluşturan kuralların ref alanları için aşağıdaki gibi güncelleştirilmiş olması gerekir:

ref e.F ref-safe-context şeklinde bir ifade:

  1. F bir ref alanıysa, ref-safe-context, 'nın e'dur.
  2. e bir başvuru türündeyse, ref-safe-context, çağıran bağlamı'ye sahiptir
  3. Aksi takdirde, ref-safe-context, 'ün e'ünden alınır.

Kurallar her zaman bir refiçinde ref struct durumunu hesaba katmış olsa da, bu bir kural değişikliğini temsil etmez. Gerçekte, ref'deki Span<T> durumunun her zaman çalıştığı şekil budur ve tüketim kuralları bunu doğru şekilde hesaba katmıştır. Buradaki değişiklik, geliştiricilerin ref alanlara doğrudan erişebilmesini ve bunu Span<T>'a örtük olarak uygulanan mevcut kurallarla gerçekleştirebilmelerini sağlamaktır.

Ancak bu, ref alanların bir ref'nın ref struct olarak döndürülebileceği, ancak normal alanların döndürülemeyeceği anlamına gelir.

ref struct RS
{
    ref int _refField;
    int _field;

    // Okay: this falls into bullet one above. 
    public ref int Prop1 => ref _refField;

    // Error: This is bullet four above and the ref-safe-context of `this`
    // in a `struct` is function-member.
    public ref int Prop2 => ref _field;
}

Bu, ilk bakışta bir hata gibi görünebilir, ancak bu kasıtlı bir tasarım noktasıdır. Yine de bu, bu teklif tarafından oluşturulan yeni bir kural değildir, bunun yerine geliştiricilerin kendi Span<T> durumlarını bildirebilecekleri mevcut kuralları ref kabul etmektedir.

Daha sonra, ref alanlarının varlığını dikkate alarak referans yeniden atama kurallarının ayarlanması gerekir. Başvurunun tekrar atanması için birincil senaryo, ref struct oluşturucularının ref parametrelerini ref alanlarına atamasıdır. Destek daha genel olacaktır ancak temel senaryo budur. Bunu desteklemek için, referans yeniden atama kuralları aşağıdaki gibi ref alanlarını hesaba katacak şekilde ayarlanacak:

Referans yeniden atama kuralları

= ref işlecinin sol işleneni, bir başvuru yerel değişkenine bağlanan bir ifade, bir başvuru parametresi (thisdışında), out parametresi, veya bir başvuru alanıolmalıdır.

Bir referansın e1 = ref e2 biçiminde yeniden atanması için aşağıdakilerin her ikisi de doğru olmalıdır:

  1. e2'nin başvuru-güvenli bağlamı, en az 'in e1 kadar büyük olmalıdır.
  2. e1, e2 ile aynı güvenli bağlam'e sahip olmalıdır

Bu, istenen Span<T> oluşturucunun ek açıklama olmadan çalıştığı anlamına gelir:

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    public Span(ref T value)
    {
        // Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The 
        // safe-context of `this` is *return-only* and ref-safe-context of `value` is 
        // *caller-context* hence this is legal.
        _field = ref value;
        _length = 1;
    }
}

Başvuru yeniden atama kurallarına yapılan değişiklik, ref parametrelerin artık ref bir değerde ref struct alanı olarak yöntemden kaçabileceği anlamına gelir. uyumluluk konuları bölümünde açıklandığı gibi bu, ref parametrelerin ref alanı olarak kaçmasına yönelik olmayan mevcut API'lerin kurallarını değiştirebilir. Parametrelerin yaşam süresi kuralları yalnızca kullanımlarına değil bildirimlerine dayanır. Tüm ref ve in parametreleri, çağıran bağlam içinde ref-safe-context sahiptirler ve bu nedenle de artık ref veya ref alanı tarafından döndürülebilir. Kaçabilen veya kaçamayan ref parametreleri olan API'leri desteklemek ve böylece C# 10 çağrı sitesi semantiğini geri yüklemek için, dil sınırlı yaşam süresi ek açıklamaları ekler.

scoped modifikatör

Anahtar sözcük scoped, bir değerin ömrünü kısıtlamak için kullanılır. ref'a veya ref struct olan ve sırasıyla ref-güvenli-bağlam veya güvenli bağlam ömrünü işlev üyesinekısıtlama etkisi olan bir değere uygulanabilir. Örneğin:

Parametre veya Yerel ref-safe bağlamı güvenli bağlam
Span<int> s İşlev üyesi arayan bağlamı
scoped Span<int> s İşlev üyesi İşlev üyesi
ref Span<int> s arayan bağlamı arayan bağlamı
scoped ref Span<int> s İşlev üyesi arayan bağlamı

Bu ilişkide, bir değerin ref-güvenli bağlamı hiçbir zaman güvenli bağlamkadar geniş olamaz.

Bu, C# 11'deki API'lerin C# 10 ile aynı kurallara sahip olacak şekilde açıklama eklemesine olanak tanır:

Span<int> CreateSpan(scoped ref int parameter)
{
    // Just as with C# 10, the implementation of this method isn't relevant to callers.
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 and legal in C# 11 due to scoped ref
    return CreateSpan(ref parameter);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

scoped ek açıklaması, bir this'nin struct parametresinin artık scoped ref Tolarak tanımlanabileceği anlamına gelir. Daha önce, diğer ref parametrelerinden farklı ref-safe-context kuralları olan ref parametresi olarak kurallarda özel olarak belirtilmesi gerekiyordu (alıcıyı güvenli bağlam kurallarına dahil etmeye veya dışlamaya yönelik tüm başvurulara bakın). Artık kurallar boyunca genel bir kavram olarak ifade edilebilir ve bu da onları daha da basitleştirir.

scoped ek açıklaması aşağıdaki konumlara da uygulanabilir:

  • locals: Bu açıklama, başlatıcının ömründen bağımsız olarak yerelin güvenli bağlamya da ref ömrüne ya da işlev üyesi ömrüne ayarlanmasını sağlar.
Span<int> ScopedLocalExamples()
{
    // Error: `span` has a safe-context of *function-member*. That is true even though the 
    // initializer has a safe-context of *caller-context*. The annotation overrides the 
    // initializer
    scoped Span<int> span = default;
    return span;

    // Okay: the initializer has safe-context of *caller-context* hence so does `span2` 
    // and the return is legal.
    Span<int> span2 = default;
    return span2;

    // The declarations of `span3` and `span4` are functionally identical because the 
    // initializer has a safe-context of *function-member* meaning the `scoped` annotation
    // is effectively implied on `span3`
    Span<int> span3 = stackalloc int[42];
    scoped Span<int> span4 = stackalloc int[42];
}

Yerel için diğer kullanımlar aşağıdaele alınıyor.

scoped ek açıklaması, dönüşler, alanlar, dizi öğeleri gibi başka hiçbir yere uygulanamaz. Ayrıca, scoped, herhangi bir ref, in veya out'e uygulandığında etkili olmaz; yalnızca ref structdeğerlere uygulandığında etkisi olur. scoped int gibi bildirimlerin bulunmasının hiçbir etkisi yoktur çünkü her zaman ref struct olmayan bir şey geri döndürmek güvenlidir. Derleyici, geliştirici karışıklığını önlemek için bu tür durumlar için bir tanılama oluşturur.

out parametrelerinin davranışını değiştirme

ref ve in parametrelerinin ref alanları olarak döndürülebilir hale getirilmesinin uyumluluk değişikliğinin etkisini daha da sınırlamak için dil, parametrelerinin varsayılan out değerini işlev üyesiolarak değiştirir. Etkin bir şekilde out parametreleri örtük olarak scoped out, gelecekte. Uyumluluk açısından bakıldığında bu, reftarafından döndürülemeyecekleri anlamına gelir:

ref int Sneaky(out int i) 
{
    i = 42;

    // Error: ref-safe-context of out is now function-member
    return ref i;
}

Bu, ref struct değerleri döndüren ve out parametreleri olan API'lerin esnekliğini artırır çünkü artık başvuruyla yakalanan parametreyi dikkate almak zorunda değildir. Bu, okuyucu stili API'lerde yaygın bir desen olduğundan önemlidir:

Span<byte> Read(Span<byte> buffer, out int read)
{
    // .. 
}

Span<byte> Use()
{
    var buffer = new byte[256];

    // If we keep current `out` ref-safe-context this is an error. The language must consider
    // the `read` parameter as returnable as a `ref` field
    //
    // If we change `out` ref-safe-context this is legal. The language does not consider the 
    // `read` parameter to be returnable hence this is safe
    int read;
    return Read(buffer, out read);
}

Dil artık bir out parametresine geçirilen bağımsız değişkenlerin döndürülebilir olmasını dikkate almayacaktır. out parametresine girilenleri döndürülebilir olarak kabul etmek, geliştiriciler için son derece kafa karıştırıcıydı. Temel olarak, out'e saygı göstermeyen diller dışında asla kullanılmayan değerin çağıran tarafından geçirilmesini geliştiricilerin göz önünde bulundurmasını zorunlu kılarak out amacını alt eder. ref struct destekleyen ileriye dönük diller, bir out parametresine geçirilen özgün değerin hiçbir zaman okunmadığından emin olmalıdır.

C# bunu, kesin atama kuralları aracılığıyla başarıyor. Bu, hem başvuru güvenli bağlam kurallarımıza uyar hem de out parametre değerlerini atayan ve ardından döndüren mevcut kodun çalışmasına izin verir.

Span<int> StrangeButLegal(out Span<int> span)
{
    span = default;
    return span;
}

Bu değişiklikler, out parametresine sağlanan argümanın, yöntem çağrılarına güvenli bağlam veya ref-güvenli bağlam değerleri eklemediği anlamına gelir. Bu, ref alanlarının genel uyumluluk etkisini önemli ölçüde azaltır ve geliştiricilerin outhakkında düşünme şeklini basitleştirir. Bir out parametresinin bağımsız değişkeni, dönüş değerine etki etmez, sadece bir çıktı olarak kullanılır.

Bildirim ifadelerinin güvenli bağlamı içinde ilişkilendir.

bir bağımsız değişkenden (out) veya yapısızlaştırmadan (M(x, out var y)) gelen bir bildirim değişkeninin (var x, var y) = M(), aşağıdakilerin en dar olanıdır .

  • çağıran bağlamı
  • out değişkeni scopedolarak işaretlenirse, o zaman bildirim bloğu (yani işlev üyesi veya daha dar).
  • out değişkeninin türü ref structise, alıcı dahil olmak üzere, kapsayıcı çağrıdaki tüm bağımsız değişkenleri dikkate alın.
    • Herhangi bir argümanın, ilgili parametresinin olmadığı ve yalnızca dönüş veya daha geniş güvenli bağlam olduğu durumlarda güvenli bağlam
    • herhangi bir bağımsız değişkenin ref-safe-context, ilgili parametresinin yalnızca dönüş veya daha geniş ref-safe-context olduğu durumlarda

Ayrıca bkz. bildirim ifadelerinin güvenli bağlam örnekleri.

Örtük olarak scoped parametreleri

Genel olarak örtük olarak refolarak bildirilen iki scoped konum vardır:

  • this örnek yönteminde struct
  • out Parametreleri

Referans güvenli bağlam kuralları scoped ref ve refşeklinde yazılacaktır. Güvenli başvuru bağlamı için, in parametresi ref'e ve out ise scoped ref'e eşdeğerdir. Hem in hem de out yalnızca kuralın semantiği için önemli olduğunda özel olarak çağrılır. Aksi takdirde yalnızca sırasıyla ref ve scoped ref kabul edilirler.

parametrelere karşılık gelen ref-safe-context argümanlar tartışılırken, bunlar belirtimde argümanlar olarak genelleştirilir. Argüman bir lvalue ise, ref-safe-context lvalue'nun bağlamıdır; aksi takdirdeişlev-üyesi olur. Yine in yalnızca geçerli kuralın semantiği için önemli olduğunda burada çağrılır.

Yalnızca dönüş güvenli bir bağlam

Tasarım, yeni bir güvenli bağlamın eklenmesini de gerektirir: yalnızca dönüş. Bu, çağıran bağlamı benzerdir, ancak yalnızca deyimi aracılığıyla döndürülebilir .

yalnızca dönüş'in ayrıntıları, işlev üyesi'ten daha büyük, ancak çağıran bağlam'ten daha küçük bir bağlamdır. return ifadesine sağlanan ifade, en az yalnızca bir dönüşiçermelidir. Mevcut kuralların çoğu bu şekilde ortaya çıkar. Örneğin, güvenli bağlam yalnızca dönüş olan bir ifadeden parametresine atama işlemi, parametresinin çağıran bağlam küçük olduğundan başarısız olur. Bu yeni kaçış bağlamına olan ihtiyaç aşağıda tartışılacaktır.

Varsayılan olarak yalnızca geri dönüş olarak kullanılan üç konum bulunmaktadır:

  • bir veya parametresi yalnızca dönüşref-safe-context sahip olur. Bu, ref structaptalca döngüsel atama sorunlarını önlemek için kısmen yapılır. Modeli basitleştirmek ve uyumluluk değişikliklerini en aza indirmek için tekdüzen olarak yapılır.
  • bir için parametresi, yalnızca dönüşgüvenli bağlam olacaktır. Bu, iade ve out eşit şekilde ifade edilebilir olmasını sağlar. out örtük olarak scoped olduğundan, başvuru-güvenli bağlam hâlâ güvenli bağlam'ten daha küçük olduğundan, bu saçma döngüsel atama sorunu yoktur.
  • oluşturucu için bir parametresi, yalnızca dönüşgüvenli bağlam içerir. Bunun out parametreleri olarak modellenmesi nedeniyle ortaya çıkmasıdır.

Bir yöntemden veya lambdadan açıkça değer döndüren herhangi bir ifade veya deyim, güvenli bağlamve varsa ref-güvenli bağlam, en az sadece dönüşolmalıdır. Buna return deyimleri, gövdeli ifade üyeleri ve lambda ifadeleri dahildir.

Benzer şekilde, herhangi bir out'e atama yapıldığında, en az dönüş içingüvenli bağlam olmalıdır. Ancak bu özel bir durum değildir, yalnızca mevcut atama kurallarından takip edilir.

Not: Türü ref struct türünde olmayan bir ifade, her zaman güvenli bağlam içinde çağıran bağlamsahiptir.

Yöntem çağırma kuralları

Yöntem çağırma için referans güvenli bağlam kuralları çeşitli şekillerde güncellenecek. Birincisi, scoped bağımsız değişkenler üzerindeki etkisini tanımaktır. exprparametresine geçirilen belirli bir bağımsız değişken p için:

  1. Eğer pscoped ref ise, expr bağımsız değişkenler göz önünde bulundurulduğunda ref-safe-context katkıda bulunmaz.
  2. Eğer pscoped ise, expr bağımsız değişkenler göz önünde bulundurulduğunda güvenli-bağlam'e katkıda bulunmaz.
  3. Eğer pout ise, exprref-safe-context veya güvenli bağlam katkı sağlamaz, daha fazla ayrıntı

"Katkıda bulunmaz" ifadesi, başvuru-güvenli bağlam veya yöntemin güvenli bağlam değeri hesaplanırken, sırasıyla bağımsız değişkenlerin dikkate alınmadığı anlamına gelir. Çünkü scoped ek açıklaması bunu engellediğinden, değerler bu yaşam süresine katkıda bulunamaz.

Yöntem çağırma kuralları artık basitleştirilebilir. Alıcının artık özel bir muamele görmesi gerekmez, struct durumunda artık yalnızca bir scoped ref T'dir. Değer kurallarının, ref alanı dönüşlerini hesaba katacak şekilde değişmesi gerekiyor.

e1.M(e2, ...) başvuru-başvuru yapısı döndürmediği M()yöntem çağırmasından kaynaklanan bir değer, aşağıdakilerin en darından alınan güvenli bağlam sahiptir:

  1. çağıran bağlamı
  2. ref struct olan dönüş, tüm bağımsız değişken ifadelerinin katkıda bulunduğu güvenli bağlam olduğunda
  3. Dönüş ref struct olduğunda, tüm bağımsız değişkenlerin katkıda bulunduğu ref

M() başvuru-başvuru yapısı döndürüyorsa, güvenli bağlam, başvuru-başvuru yapısı olan tüm bağımsız değişkenlerin güvenli bağlam ile aynıdır. Birden çok bağımsız değişkenin farklı güvenli bağlamı varsa, yöntem bağımsız değişkenlerinineşleşmesi gerektiğinden hatadır.

ref çağırma kuralları şu şekilde basitleştirilebilir:

ref-to-ref-yapı döndürmediği sürece, yöntem çağrısından kaynaklanan bir değer, aşağıdaki bağlamların en darı olan ref-safe-context 'dir:

  1. çağıran bağlamı
  2. Tüm bağımsız değişken ifadelerinin katkıda bulunduğu güvenli bağlam
  3. Tüm bağımsız değişkenlerin katkıda bulunduğu ref

M() referans-referans yapısını döndürüyorsa, referans-güvenli bağlam, referans-referans yapısı olan tüm bağımsız değişkenlerin katkıda bulunduğu en dar referans-güvenli bağlam olur.

Bu kural artık istenen yöntemlerin iki değişkenini tanımlamamıza olanak tanır:

Span<int> CreateWithoutCapture(scoped ref int value)
{
    // Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *function-member* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> CreateAndCapture(ref int value)
{
    // Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *caller-context* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
    // Okay: the safe-context of `span` is *caller-context* hence this is legal.
    return span;

    // Okay: the local `refLocal` has a ref-safe-context of *function-member* and a 
    // safe-context of *caller-context*. In the call below it is passed to a 
    // parameter that is `scoped ref` which means it does not contribute 
    // ref-safe-context. It only contributes its safe-context hence the returned
    // rvalue ends up as safe-context of *caller-context*
    Span<int> local = default;
    ref Span<int> refLocal = ref local;
    return ComplexScopedRefExample(ref refLocal);

    // Error: similar analysis as above but the safe-context of `stackLocal` is 
    // *function-member* hence this is illegal
    Span<int> stackLocal = stackalloc int[42];
    return ComplexScopedRefExample(ref stackLocal);
}

Nesne başlatıcıları için kurallar

Nesne başlatıcı ifadesinin güvenli bağlam en dar değerdir:

  1. Oluşturucu çağrısının güvenli bağlamı .
  2. güvenli bağlam ve ref-güvenli bağlam olan ve alıcıya kaçabilen üye başlatıcı dizinleyicilere yönelik bağımsız değişkenler.
  3. Üye başlatıcılardaki atamaların rhs değerinin salt okunur olmayan ayarlayıcılara güvenli bağlam veya başvuru ataması durumunda ref-safe-context .

Bunu modellemenin başka bir yolu, alıcıya atanabilecek üye başlatıcıya yönelik bağımsız değişkenleri oluşturucuya bağımsız değişken olarak düşünmektir. Bunun nedeni, üye başlatıcının etkili bir oluşturucu çağrısı olmasıdır.

Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
    Field = stackSpan;
}

// Can be modeled as 
var x = new S(ref heapSpan, stackSpan);

Bu modelleme, MAMM'imizin üye başlatıcılarını özellikle dikkate alması gerektiğini gösterdiğinden önemlidir. Daha dar güvenli bağlam daha yüksek bir değere atanmasına izin verdiğinden, bu özel durumun geçersiz olması gerektiğini düşünün.

Yöntem bağımsız değişkenleri eşleşmelidir

ref alanlarının varlığı, yöntem bağımsız değişkenlerinin etrafındaki kuralların güncelleştirilmesi gerektiği anlamına gelir; ref parametresi artık yöntemin ref struct bağımsız değişkeninde bir alan olarak depolanabilir. Daha önce kural, yalnızca başka bir ref struct'ın alan olarak depolanmasını dikkate almak zorundaydı. Bu etkinin uyumluluk konularıüzerindeki etkisi tartışılmaktadır. Yeni kural...

Herhangi bir yöntem çağırma e.M(a1, a2, ... aN)

  1. En dar güvenli bağlam'i 'dan hesaplayın:
    • arayan bağlamı
    • Tüm bağımsız değişkenlerin güvenli bağlamı
    • İlgili parametreleri çağıran bağlamıref-safe-context sahip olan tüm başvuru bağımsız değişkenlerinin ref-safe-context
  2. ref türlerinin tüm ref struct bağımsız değişkenleri, güvenli bağlamsahip bir değer tarafından atanabilir olmalıdır. Bu, 'ın ve 'ü içerecek şekilde genel bir duruma dönüşmemesi halidir.

Herhangi bir yöntem çağırma e.M(a1, a2, ... aN)

  1. En dar güvenli bağlam'i 'dan hesaplayın:
    • arayan bağlamı
    • Tüm bağımsız değişkenlerin güvenli bağlamı
    • karşılık gelen parametreleri olmayan tüm başvuru bağımsız değişkenlerinin scoped
  2. out türlerinin tüm ref struct bağımsız değişkenleri, güvenli bağlamsahip bir değer tarafından atanabilir olmalıdır.

scoped varlığı, geliştiricilerin scopedolarak döndürülmeyen parametreleri işaretleyerek bu kuralın oluşturduğu sürtüşmeleri azaltmasına olanak tanır. Bu, yukarıdaki her iki durumda da argümanları (1) çıkarır ve çağıranlara daha fazla esneklik sağlar.

Bu değişikliğin etkisi, 'da'in altında daha ayrıntılı ele alınmaktadır. Genel olarak bu, geliştiricilerin scopedile kaçış içermeyen ref benzeri değerlere açıklama ekleyerek çağrı noktalarını daha esnek hale getirmesine olanak sağlar.

Parametre kapsamı varyansı

Parametrelerdeki scoped değiştirici ve [UnscopedRef] özniteliği (aşağıdaki bakın), nesne geçersiz kılma, arabirim uygulaması ve delegate dönüştürme kurallarımızı da etkiler. Geçersiz kılma, arabirim uygulaması veya delegate dönüştürme imzası şunları yapabilir:

  • scoped veya ref parametresine in ekleme
  • scopedref struct parametresine ekle
  • [UnscopedRef] parametresinden out'ı kaldırın
  • bir [UnscopedRef] türünün ref parametresinden ref struct kaldırın

scoped veya [UnscopedRef] ile ilgili diğer farklar uyuşmazlık olarak kabul edilir.

Derleyici, geçersiz kılmalar, arabirim uygulamaları ve temsilci dönüştürmeleri arasında güvenli olmayan kapsamlı uyuşmazlıklar için aşağıdaki durumlarda bir tanılama bildirir:

  • Yöntem, ref ekleme (outkaldırmama) uyuşmazlığı olan, ref struct türünde bir [UnscopedRef] veya scoped parametresine sahiptir. (Bu durumda, aptalca bir döngüsel atama mümkündür, bu nedenle başka parametre gerekmez.)
  • Ya da bunların ikisi de doğrudur:
    • yöntemi bir ref struct döndürür veya ref veya ref readonlydöndürür ya da yöntemin ref türünde bir out veya ref struct parametresi vardır.
    • yönteminde en az bir ek ref, inveya out parametresi ya da ref struct türünde bir parametre vardır.

Tanı aşağıdaki sebeplerle diğer durumlarda bildirilmez:

  • Bu tür imzalara sahip yöntemler geçirilen referansları yakalayamaz, bu nedenle kapsamdaki uyuşmazlık tehlikeli değildir.
  • Bunlar arasında çok yaygın ve basit senaryolar (örneğin, out yöntem imzalarında kullanılan düz eski TryParse parametreleri) bulunmaktadır. Dil sürümü 11'de kullanıldıkları için (ve bu nedenle out parametresinin kapsamı farklı olduğundan) kapsam farklılıklarının bildirilmesi kafa karıştırıcı olabilir.

Eşleşmeyen imzaların her ikisi de C#11 başvuru güvenli bağlam kurallarını kullanıyorsa tanılama hata bildirilir; aksi takdirde, tanılama uyarı'dir.

C#7.2 başvuru güvenli bağlam kurallarıyla derlenen ve scoped'ın mevcut olmadığı bir modülde kapsam uyuşmazlığı uyarısı bildirilebilir. Bazı durumlarda, eşleşmeyen diğer imza değiştirilemiyorsa uyarının gizlenmesi gerekebilir.

scoped değiştiricisi ve [UnscopedRef] özniteliği de yöntem imzaları üzerinde aşağıdaki etkilere sahiptir:

  • scoped değiştirici ve [UnscopedRef] özniteliği gizlemeyi etkilemez
  • Aşırı yüklemeler yalnızca scoped veya [UnscopedRef] üzerinde farklılık gösteremez.

ref alanı ve scoped bölümü uzun olduğundan, önerilen köklü değişikliklerin kısa bir özetiyle kapatmak istedim.

  • ref-safe-context olan bir değer, çağıran bağlamı için ref veya ref alanı tarafından döndürülebilir.
  • Bir out parametresi güvenli bağlamişlev üyesi'ün parçası olabilir.

Ayrıntılı Notlar:

  • ref alanı yalnızca bir ref struct içinde bildirilebilir
  • ref alanı static, volatile veya const bildirilemez
  • ref alanı, bir ref struct türe sahip olamaz.
  • Referans derlemesi oluşturma süreci, ref içinde bir ref struct alanının mevcudiyetini korumalıdır.
  • readonly ref struct, ref alanlarını readonly ref olarak bildirmelidir.
  • Başv değerleri için, scoped değiştirici in, outveya ref'ten önce yer almalıdır.
  • Span güvenlik kuralları belgesi, bu belgede açıklandığı gibi güncelleştirilecek
  • Yeni başvuru güvenli bağlam kuralları şu durumlarda geçerli olur:
    • Çekirdek kitaplık, ref alanları için desteği gösteren özellik bayrağını içerir
    • langversion değeri 11 veya üzeridir

Sözdizimi

13.6.2 Yerel değişken bildirimleri: 'scoped'?eklendi.

local_variable_declaration
    : 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
    ;

local_variable_mode_modifier
    : 'ref' 'readonly'?
    ;

13.9.4 for deyimi: 'scoped'?local_variable_declaration dolaylı olarak eklendi.

13.9.5 foreach deyimi: 'scoped'?eklendi.

foreach_statement
    : 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
      embedded_statement
    ;

12.6.2 Bağımsız değişken listeleri: 'scoped'? bildirim değişkeni için out eklendi.

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' ('scoped'? local_variable_type)? identifier
    ;

12.7 Dekonstrüksiyon ifadeleri:

[TBD]

15.6.2 Yöntem parametreleri: 'scoped'?parameter_modifier'e eklendi.

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

parameter_modifier
    | 'this' 'scoped'? parameter_mode_modifier?
    | 'scoped' parameter_mode_modifier?
    | parameter_mode_modifier
    ;

parameter_mode_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

20.2 Temsilci bildirimleri: 'dan dolaylı olarak eklendi.

12.19 Anonim işlev ifadeleri: 'scoped'?eklendi.

explicit_anonymous_function_parameter
    : 'scoped'? anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

Gün batımı kısıtlanmış türleri

Derleyici, büyük ölçüde belgelenmemiş bir "kısıtlanmış türler" kümesi kavramına sahiptir. C# 1.0'da davranışlarını ifade etmenin genel amaçlı bir yolu olmadığından bu türlere özel bir durum verilmiştir. En önemlisi, türlerin yürütme yığınına başvurular içerebileceği gerçeğidir. Bunun yerine derleyici bunların özel bilgisine sahipti ve kullanımlarını her zaman güvenli olacak yollarla kısıtlamıştı: izin verilmeyen iadeler, dizi öğeleri olarak kullanılamaz, genel öğelerde kullanamaz, vb...

ref alanları kullanılabilir ve ref struct destekleyecek şekilde genişletildikten sonra bu türler ref struct ve ref alanlarının birleşimi kullanılarak C# dilinde doğru şekilde tanımlanabilir. Böylece, derleyici bir çalışma zamanının ref alanlarını desteklediğini algıladığında, artık kısıtlanmış türler kavramına sahip olmaz. Bunun yerine kodda tanımlandığı gibi türleri kullanır.

Bunu desteklemek için başvuru güvenli bağlam kurallarımız aşağıdaki gibi güncelleştirilecektir:

  • __makeref, imza static TypedReference __makeref<T>(ref T value) ile bir yöntem olarak ele alınacaktır
  • __refvalue, static ref T __refvalue<T>(TypedReference tr)imzası olan bir yöntem olarak ele alınacaktır. __refvalue(tr, int) ifadesi, tür parametresi olarak ikinci bağımsız değişkeni etkili bir şekilde kullanır.
  • __arglist, bir parametre olarak, işlev üyesi'nın ref-safe-context ve güvenli bağlam'si olacaktır.
  • bir ifade olarak __arglist(...), ref-safe-context ve güvenli bağlam ile işlev üyesiolacaktır.

Uyumlu çalışma zamanları, TypedReference, RuntimeArgumentHandle ve ArgIteratorref structolarak tanımlanmasını sağlar. Daha da önemlisi, TypedReference, olası herhangi bir tür için ref'ye kadar genişleyen bir ref struct alanı olduğuna göre değerlendirilmelidir (herhangi bir değeri depolayabilir). Bu, yukarıdaki kurallarla birlikte yığına yapılan referansların ömrünü aşmamasını sağlar.

Not: Tam anlamıyla, bu dilin bir parçası değil, bir derleyici uygulama ayrıntısıdır. Ancak ref alanlarıyla olan ilişki göz önüne alındığında, basitlik için dil önerisine dahil ediliyor.

Kapsamsız temin etme

En önemli sürtünme noktalarından biri, reförneği üyelerinde struct tarafından alanların geri döndürülememesidir. Bu, geliştiricilerin ref döndüren yöntemler / özellikler oluşturamadıklarından ve alanları doğrudan kullanıma açmak zorunda olduğu anlamına gelir. ref'de genellikle en çok istenen, struct dönüşlerinin yararlılığını azaltır.

struct S
{
    int _field;

    // Error: this, and hence _field, can't return by ref
    public ref int Prop => ref _field;
}

Bu varsayılan için rasyonel makuldür, ancak başvuruyla struct kaçış this doğal olarak yanlış bir şey yoktur, yalnızca başvuru güvenli bağlam kuralları tarafından seçilen varsayılan değerdir.

Bu sorunu çözmek için programlama dili, scoped'i destekleyerek UnscopedRefAttribute yaşam süresi ek açıklamasının karşıtını sunar. Herhangi bir ref'a uygulanabilir ve böylece ref-safe-context varsayılan ayarından bir düzey daha geniş olacak şekilde değiştirilir. Örneğin:

UnscopedRef şu objeye uygulandı: [context or object name] Özgün ref-safe-context Yeni ref-safe-context
örnek üyesi işlev üyesi sadece iade
in / ref parametresi sadece iade çağıran bağlamı
out parametresi işlev üyesi sadece iade

Bir [UnscopedRef] örnek yöntemi üzerinde struct uygularken, örtük this parametresini değiştirme etkisi yaratır. Bu, this'ın aynı türdeki ek açıklamasız bir ref olarak davrandığı anlamına gelir.

struct S
{
    int field; 

    // Error: `field` has the ref-safe-context of `this` which is *function-member* because 
    // it is a `scoped ref`
    ref int Prop1 => ref field;

    // Okay: `field` has the ref-safe-context of `this` which is *caller-context* because 
    // it is a `ref`
    [UnscopedRef] ref int Prop1 => ref field;
}

Açıklama, C# 10'un davranışını geri yüklemek amacıyla out parametrelerine de yerleştirilebilir.

ref int SneakyOut([UnscopedRef] out int i)
{
    i = 42;
    return ref i;
}

Referans güvenli bağlam kurallarının amaçları doğrultusunda, böyle bir [UnscopedRef] out yalnızca refolarak kabul edilir. in'nin yaşam süresi için ref olarak kabul edilmesine benzer.

[UnscopedRef] açıklamasına initiçindeki struct üyeleri ve oluşturucularında izin verilmeyecek. Bu üyeler, ref üyeleri değiştirilebilir olarak gördükleri için readonly semantik açısından zaten özeldir. Bu, ref'ın bu üyelere basit bir refolarak, ref readonlydeğil, göründüğü anlamına gelir. Oluşturucuların ve init'ın sınırları içinde buna izin verilir. [UnscopedRef]'ı izin vermek, böyle bir ref'in readonly semantik gerçekleştikten sonra yanlış bir şekilde oluşturucunun dışına kaçmasına ve mutasyona olanak tanır.

Öznitelik türü aşağıdaki tanıma sahip olacaktır:

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(
        AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
        AllowMultiple = false,
        Inherited = false)]
    public sealed class UnscopedRefAttribute : Attribute
    {
    }
}

Ayrıntılı Notlar:

  • [UnscopedRef] ile açıklama eklenmiş bir örnek yöntem veya özellik, çağıran bağlamıolarak belirlenmiş olan thisref-safe-context'ye sahiptir.
  • [UnscopedRef] ile açıklanmış bir üye bir arabirimi uygulayamaz.
  • [UnscopedRef]'ı kullanmak bir hatadır.
    • Bildirilmemiş bir üye struct üzerinde
    • static üyesi, init üyesi veya struct kurucu
    • scoped olarak işaretlenmiş bir parametre
    • Değere göre geçirilen bir parametre
    • Referansla geçirilen, örtük olarak kapsamı belirlenmemiş bir parametre

ScopedRefAttribute

scoped ek açıklamaları, tür System.Runtime.CompilerServices.ScopedRefAttribute özniteliği aracılığıyla meta veriler halinde yayılır. Özniteliği ad alanı nitelenmiş adıyla eşleştirilir, bu nedenle tanımın belirli bir derlemede görünmesi gerekmez.

ScopedRefAttribute türü yalnızca derleyici kullanımı içindir; kaynakta buna izin verilmez. Tür bildirimi, derlemeye henüz dahil değilse derleyici tarafından sentezlenmiştir.

Türün tanımı şu şekilde olacaktır:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    internal sealed class ScopedRefAttribute : Attribute
    {
    }
}

Derleyici bu özniteliği scoped söz dizimi ile parametresinde yayar. Bu, yalnızca söz dizimi değerin varsayılan durumundan farklı olmasını sağlarsa yayılır. Örneğin scoped out hiçbir özniteliğin yayılmamasına neden olur.

RefGüvenlikKurallarıNiteliği

C#7.2 ile C#11 arasındaki başvuru güvenli bağlamı kurallarında çeşitli farklılıklar vardır. Bu farklardan herhangi biri, C#10 veya önceki sürümlerle derlenen referanslarla C#11 ile yeniden derlendiği zaman hatalara neden olabilir.

  1. kapsamsız ref/in/out parametreleri, C#11'de bir ref'nın ref struct alanı olarak bir yöntem çağrısından kaçabilir; bu, C#7.2'de gerçekleşmez.
  2. out parametreleri C#11'de örtük olarak kapsam dahilindedir ve C#7.2'de kapsam dışıdır.
  3. ref türlerine /inref struct parametrelerinin kapsamı C#11'de örtük olarak ve C#7.2'de kapsamsız

C#11 ile yeniden derleme sırasında uyumsuz değişikliklerin olasılığını azaltmak için C#11 derleyicisini, yöntem bildirimini çözümlemek için kullanılan kurallarla () eşleşen yöntem çağrısı kuralları ( ve için) başvuru güvenli bağlam kurallarını () kullanacak şekilde güncelleyeceğiz. Temel olarak, C#11 derleyicisi, eski bir derleyiciyle derlenen bir yönteme yapılan çağrıyı analiz ederken C#7.2'deki "ref veya güvenli bağlam" kurallarını kullanır.

Bunu etkinleştirmek için, modül [module: RefSafetyRules(11)] veya üzeri ile derlendiğinde veya -langversion:11 alanları için özellik bayrağını içeren bir corlib ile derlendiğinde derleyici yeni bir ref özniteliği yayar.

Özniteliğe ait bağımsız değişken, modül derlendiğinde kullanılan başvuru güvenli bağlamı kurallarının dil sürümünü gösterir. Sürüm şu anda derleyiciye geçirilen gerçek dil sürümüne bakılmaksızın 11 olarak sabitlenmiştir.

Beklenti, derleyicinin gelecekteki sürümlerinin referans güvenli bağlam kurallarını güncellemesi ve öznitelikleri farklı sürümlerle belirgin şekilde oluşturmasıdır.

Derleyici, dışında bir içeren bir içeren bir modülü yüklerse, bu modülde bildirilen yöntemlere yönelik çağrılar varsa, derleyici tanınmayan sürüm için bir uyarı bildirir.

C#11 derleyicisi bir yöntem çağrısı çözümlediğinde:

  • Yöntem bildirimini içeren modül [module: RefSafetyRules(version)]içeriyorsa, versionne olursa olsun yöntem çağrısı C#11 kurallarıyla analiz edilir.
  • Yöntem bildirimini içeren modül kaynaktan geliyorsa ve -langversion:11 veya ref alanları için özellik bayrağını içeren bir corlib ile derlenmişse, yöntem çağrısı C#11 kurallarıyla analiz edilir.
  • Yöntem bildirimini içeren modül System.Runtime { ver: 7.0 }başvuruda bulunursa, yöntem çağrısı C#11 kurallarıyla analiz edilir. Bu kural, C#11 / .NET 7'nin önceki önizlemeleriyle derlenen modüller için geçici bir azaltmadır ve daha sonra kaldırılacaktır.
  • Aksi takdirde, yöntem çağrısı C#7.2 kurallarıyla analiz edilir.

C#11 öncesi derleyici tüm RefSafetyRulesAttribute yoksayar ve yöntem çağrılarını yalnızca C#7.2 kurallarıyla analiz eder.

RefSafetyRulesAttribute, ad alanıyla nitelendirilmiş ismiyle eşleştirilir, bu nedenle tanımın belirli bir derlemede bulunması gerekmez.

RefSafetyRulesAttribute türü yalnızca derleyici kullanımı içindir; kaynakta buna izin verilmez. Tür bildirimi, derlemeye henüz dahil değilse derleyici tarafından sentezlenmiştir.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
    internal sealed class RefSafetyRulesAttribute : Attribute
    {
        public RefSafetyRulesAttribute(int version) { Version = version; }
        public readonly int Version;
    }
}

Güvenli sabit boyutlu arabellekler

Güvenli sabit boyutlu arabellekler C# 11'de yer almadı. Bu özellik C# uygulamasının gelecekteki bir sürümünde uygulanabilir.

Dil, sabit boyutlu diziler üzerindeki kısıtlamaları rahatlatır; böylece bunlar güvenli kodda bildirilebilir ve öğe türü yönetilebilir veya yönetilemez. Bu, aşağıdaki gibi türleri yasal hale getirir:

internal struct CharBuffer
{
    internal char Data[128];
}

Bu bildirimler, unsafe karşılık gelen parçalarına çok benzer şekilde, içeren türdeki N öğeler dizisini tanımlar. Bu üyelere bir dizin oluşturucu ile erişilebilir ve Span<T> ve ReadOnlySpan<T> örneklerine de dönüştürülebilir.

fixed türünde bir T arabelleğe dizin oluştururken kapsayıcının readonly durumu dikkate alınmalıdır. Kapsayıcı readonly ise dizin oluşturucu ref readonly T döndürür, aksi takdirde ref Tdöndürür.

Dizin oluşturucu olmadan fixed arabelleğe erişmenin doğal türü yoktur ancak Span<T> türlerine dönüştürülebilir. Kapsayıcı olduğunda, arabellek örtük olarak 'e dönüştürülebilir, aksi takdirde veya 'e dönüştürülebilir ( dönüşümüiçin daha iyi kabul edilir).

Sonuçta elde edilen Span<T> örneği, fixed arabelleğinde bildirilen boyuta eşit bir uzunluğa sahip olur. Döndürülen değerin güvenli bağlamı, arkadaki verilere alan olarak erişildiğinde olacağı gibi, kapsayıcının güvenli bağlamı eşit olacaktır.

Öğe türünün fixed bir türdeki her T bildirimi için dil, dönüş türü getolan karşılık gelen ref T yalnızca dizin oluşturucu yöntemini oluşturur. Uygulama bildirim türündeki alanları döndüreceği için dizin oluşturucuya [UnscopedRef] özniteliğiyle ek açıklama eklenir. Üyenin erişilebilirliği, fixed alanındaki erişilebilirlikle eşleşecektir.

Örneğin, CharBuffer.Data için dizin oluşturucunun imzası aşağıdaki gibi olacaktır:

[UnscopedRef] internal ref char DataIndexer(int index) => ...;

Sağlanan dizin, fixed dizisinin bildirilen sınırlarının dışındaysa bir IndexOutOfRangeException atılır. Sabit bir değer sağlanırsa, bu değer uygun öğeye doğrudan başvuru ile değiştirilir. Sabit, bildirilen sınırların dışında değilse, bu durumda bir derleme zamanı hatası oluşur.

Ayrıca, değer olarak fixed ve get işlemleri sağlayan her set arabelleği için bir adlandırılmış erişimci de üretilecektir. Bu, fixed arabelleklerinin, ref erişimcisiyle birlikte, byval get ve set işlemlerine sahip olarak mevcut dizi semantiğine daha yakın hale geleceği anlamına gelir. Bu, derleyicilerin fixed arabelleklerini kullanan kod üretirken, dizilere uyguladıkları esneklikle aynı esnekliği gösterebileceği anlamına gelir. Bu, await gibi işlemlerin fixed arabellekler üzerinde daha kolay gerçekleştirilmesini sağlamalıdır.

Bu ayrıca, fixed arabelleklerin diğer dillerden daha kolay kullanılabilmesini sağlama gibi bir ek avantaj da sunar. Adlandırılmış dizin oluşturucular, .NET'in 1.0 sürümünden bu yana var olan bir özelliktir. Adlandırılmış bir dizin oluşturucuyu doğrudan kullanamayan diller bile bunları genellikle tüketebilir (C# aslında bunun iyi bir örneğidir).

Arabellek için arka plan depolama alanı [InlineArray] özniteliği kullanılarak oluşturulacak. Bu, sorun 12320'da ele alınan ve özellikle aynı türdeki alanların sırasının verimli bir şekilde bildirilmesine olanak tanıyan bir mekanizmadır. Bu özel sorun hala etkin bir tartışma aşamasındadır ve bu özelliğin uygulanmasının tartışma devam edene kadar devam edeceği beklentisi vardır.

ref ve new ifadelerinde with değerleri olan başlatıcılar

12.8.17.3 Nesne başlatıcılarıbölümünde, dil bilgisini şu şekilde güncelleştiriyoruz:

initializer_value
    : 'ref' expression // added
    | expression
    | object_or_collection_initializer
    ;

ifadesi bölümünde dil bilgisini şu şekilde güncelleştiriyoruz:

member_initializer
    : identifier '=' 'ref' expression // added
    | identifier '=' expression
    ;

Atamanın sol işleneni, bir ref alanına bağlanan bir ifade olmalıdır.
Sağ işlenen, sol işlenenle aynı türde bir değeri belirten bir lvalue üreten bir ifade olmalıdır.

başvuru yerel yeniden atamabenzer bir kural ekliyoruz:
Sol operand bir yazılabilir başvuru türüyse (yani, ref readonly alanı dışında herhangi bir öğeyi belirlerse), sağ operand yazılabilir bir lvalue olmalıdır.

oluşturucu çağrıları için kaçış kuralları, olarak kalır.

Oluşturucu çağıran bir new ifadesi, oluşturmakta olan türü döndürmek için kabul edilen bir yöntem çağrısıyla aynı kurallara uyar.

Yani yukarıda güncelleştirilen çağırma yöntemi kuralları:

Yöntem çağırımının e1.M(e2, ...) sonucunda elde edilen rvalue, aşağıdaki bağlamların en küçüğü olan güvenli bağlam'ye sahiptir:

  1. çağıran bağlamı
  2. Tüm bağımsız değişken ifadelerinin katkıda bulunduğu güvenli bağlam
  3. Dönüş ref struct olduğunda, tüm argümanların katkıda bulunduğu ref.

Başlatıcıları olan bir new ifadesi için, başlatıcı ifadeleri (güvenli bağlamakatkıda bulunarak) bağımsız değişken olarak sayılır ve ref başlatıcı ifadeleri (refkatkıda bulunarak) bağımsız değişkenleri olarak, özyinelemeli şekilde sayılır.

Güvenli olmayan bağlamdaki değişiklikler

İşaretçi türleri (bölüm 23.3), yönetilen türleri başvuru türü olarak kabul edecek şekilde genişletilmiştir. Bu tür işaretçi türleri yönetilen bir tür olarak yazılır ve ardından bir * belirteci gelir. Bir uyarı üretirler.

İşlecin adresi (bölüm 23.6.5) yönetilen türe sahip bir değişkeni işlenen olarak kabul edecek şekilde gevşetilir.

fixed deyimi (bölüm 23.7), yönetilen tür değişkeninin adresi olan veya yönetilen tür Töğelerine sahip bir array_type ifadesi olan T kabul etmek için gevşetilir.

Yığın ayırma başlatıcısı (bölüm 12.8.22) benzer şekilde gevşetilir.

Dikkate Alınacaklar

Geliştirme yığınının bu özelliği değerlendirirken dikkate alması gereken diğer bölümleri de vardır.

UyumlulukLa İlgili Dikkat Edilmesi Gerekenler

Bu teklifteki zorluk, bu tasarımın mevcut yayma güvenlik kurallarımızveya §9.7.2üzerindeki uyumluluk etkileridir. Destekleyen kurallar, ref struct'ın ref alanlarına sahip olma kavramını tamamen desteklerken, stackallocdışındaki API'lerin yığınla ilgili ref durumunu yakalamasına izin vermemektedir. Başvuru güvenli bağlam kurallarınınbir sabit varsayımı vardır veya form oluşturucusunun mevcut olmadığını §16.4.12.8 . Bu, güvenlik kurallarının bir ref parametresinin ref alanı olarak kaçabilmesini hesaba katmadığını ve bu nedenle aşağıdaki gibi kodlara izin verdiği anlamına gelir.

Span<int> CreateSpanOfInt()
{
    // This is legal according to the 7.2 span rules because they do not account
    // for a constructor in the form Span(ref T value) existing. 
    int local = 42;
    return new Span<int>(ref local);
}

Bir ref parametresinin yöntem çağrısından kaçması için üç yol vardır:

  1. Değere göre dönüş
  2. ref dönüşe göre
  3. ref ref struct ref / parametresi olarak döndürülen veya geçirilen out alanına göre

Mevcut kurallar yalnızca (1) ve (2) için geçerlidir. (3) değerlerini hesaba katmıyorlar, bu nedenle ref alanları olarak yerel değişkenleri döndürme gibi eksiklikler hesaba katılmaz. Bu tasarım, (3)'ü dikkate almak için kuralları değiştirmelidir. Bu, mevcut API'ler için uyumluluk üzerinde küçük bir etkiye sahip olacaktır. Özellikle aşağıdaki özelliklere sahip API'leri etkiler.

  • İmzada bir ref struct var
    • burada ref struct bir dönüş türü, ref veya out parametresidir
    • Alıcı hariç ek bir in veya ref parametresi vardır

C# 10'da bu tür API'leri çağıranların API'ye ref durum girişinin bir ref alanı olarak yakalanabileceğini hiç düşünmesi gerekmedi. Bu özellik, C# 10'da çeşitli desenlerin güvenli bir şekilde var olmasına izin verdi. Ancak, ref durumunun ref alanı olarak kaçabilmesi nedeniyle, bu desenler C# 11'de güvensiz hale gelecektir. Örneğin:

Span<int> CreateSpan(ref int parameter)
{
    // The implementation of this method is irrelevant when considering the lifetime of the 
    // returned Span<T>. The ref safe context rules only look at the method signature, not the 
    // implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
    // to escape by ref in this method
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 but would be illegal with ref fields
    return CreateSpan(ref parameter);

    // Legal in C# 10 but would be illegal with ref fields
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 but would be illegal with ref fields
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

Bu uyumluluk sonunun etkisinin çok küçük olması beklenir. Etkilenen API şekli, ref alanlarının olmamasında çok anlamlı olmadığından müşterilerin bunların çoğunu oluşturma olasılığı düşüktür. Bu API şeklini mevcut depolar üzerinde tespit etmek için araçları çalıştıran deneyler, bu iddiayı destekler. Bu biçime sahip önemli sayılara sahip tek depo dotnet/runtime ve bunun nedeni, repo'nun ref intrinsic türü aracılığıyla ByReference<T> alanlar oluşturabilmesidir.

Yine de, tasarım bu tür API'lerin varlığını hesaba katmalıdır, çünkü bu geçerli bir deseni ifade eder, ancak pek yaygın bir desen değildir. Bu nedenle tasarım, geliştiricilere C# 10'a yükseltirken mevcut yaşam süresi kurallarını geri yükleme araçları vermelidir. Geliştiricilerin ref parametrelerini ref veya ref alanı tarafından kaçamayacak şekilde açıklamalarına olanak tanıyan mekanizmalar sağlamalıdır. Bu, müşterilerin C# 11'de aynı C# 10 çağrı sitesi kurallarına sahip API'leri tanımlamasına olanak tanır.

Başvuru Derlemeleri

Bu teklifte açıklanan özellikleri kullanan bir derleme için başvuru derlemesi, başvuru güvenli bağlam bilgilerini aktaran öğeleri tutmalıdır. Bu, tüm ömür boyu açıklama niteliklerinin özgün konumlarında korunması gerektiği anlamına gelir. Bunları değiştirme veya atlama girişimleri geçersiz başvuru derlemeleri oluşturabilir.

ref alanlarını temsil etmek daha nüanslıdır. İdeal olarak bir ref alanı, diğer tüm alanlarda olduğu gibi bir başvuru derlemesinde görünür. Ancak ref alanı, meta veri biçiminde yapılan bir değişikliği temsil eder ve bu, bu meta veri değişikliğini anlamak için güncelleştirilmeyen araç zincirleriyle ilgili sorunlara neden olabilir. C++/CLI, bir ref alanı tüketirse büyük olasılıkla hata veren somut bir örnektir. Bu nedenle, ref alanların temel kitaplıklarımızdaki başvuru derlemelerinden atlanması avantajlıdır.

ref alanının tek başına başvuru güvenli bağlam kuralları üzerinde hiçbir etkisi yoktur. Somut bir örnek olarak, mevcut Span<T> tanımını ref bir alan kullanacak şekilde çevirmenin tüketim üzerinde hiçbir etkisi olmadığını düşünün. Bu nedenle ref güvenli bir şekilde atlanabilir. Ancak bir ref alanının, korunması gereken tüketim üzerinde başka etkileri de vardır:

  • ref struct alanı olan bir ref hiçbir zaman unmanaged olarak kabul edilmez
  • ref alanının türü sonsuz genel genişletme kurallarını etkiler. Bu nedenle, ref alanının türü korunması gereken bir tür parametresi içeriyorsa

Bu kurallara göre, ref structiçin geçerli bir referans montaj dönüşümü:

// Impl assembly 
ref struct S<T>
{
    ref T _field;
}

// Ref assembly 
ref struct S<T>
{
    object _o; // force managed 
    T _f; // maintain generic expansion protections
}

Ek Açıklamalar

Yaşam süreleri en doğal olarak türler kullanılarak ifade edilir. Belirli bir programın yaşam süresi, yaşam süresi türleri denetiminde güvenlidir. C# söz dizimi, değerlere ömürleri örtük olarak eklese de, burada temel kuralları açıklayan bir temel tür sistemi vardır. Tasarımdaki değişikliklerin bu kurallar açısından etkilerini tartışmak genellikle daha kolaydır, bu nedenle tartışma amacıyla buraya dahil edilirler.

Bunun 100% eksiksiz bir belge olması gerekmediğini unutmayın. Her davranışı belgeleme burada amaç değildir. Bunun yerine modelin ve modeldeki olası değişikliklerin tartışılabildiği genel bir anlayış ve ortak bir açıklama oluşturmaktır.

Genellikle yaşam süresi türleri hakkında doğrudan konuşmak gerekmez. Özel durumlar, yaşam ömürlerinin belirli "oluşturulma" sitelerine göre değişebildiği yerlerdir. Bu bir tür polimorfizmdir ve bu değişen yaşam süreleri genel parametreler olarak temsil edilen "genel yaşam süreleri" olarak adlandırılır. C# yaşam süresi genel değerlerini ifade etmek için söz dizimi sağlamaz, bu nedenle C# dilinden açık genel parametreler içeren genişletilmiş bir azaltılmış dile örtük bir "çeviri" tanımlarız.

Aşağıdaki örneklerde adlandırılmış yaşam süreleri kullanılır. sözdizimi $a, aadlı bir yaşam süresine atıfta bulunur. Tek başına bir anlamı olmayan ancak where $a : $b söz dizimi aracılığıyla diğer yaşam süreleriyle bir ilişki verilebilen bir yaşam süresidir. Bu, $a$bdönüştürülebilir olduğunu gösterir. Bunun, en az $akadar uzun bir süre olan $b'ı belirlemek olarak düşünülmesi faydalı olabilir.

Aşağıda kolaylık ve kısalık için önceden tanımlanmış birkaç yaşam süresi vardır:

  • $heap: Bu, yığında bulunan herhangi bir değerin ömrüdür. Tüm bağlamlarda ve yöntem imzalarında kullanılabilir.
  • $local: Bu, yöntem yığınında bulunan tüm değerlerin ömrüdür. fonksiyon üyesiiçin etkili bir ad yer tutucu. Yöntemlerde örtük olarak tanımlanır ve herhangi bir çıkış konumu dışında yöntem imzalarında görüntülenebilir.
  • $ro: yalnızca iadesi için ad yer tutucusu
  • $cm: çağıran bağlam için ad yer tutucusu

Yaşam süreleri arasında önceden tanımlanmış birkaç ilişki vardır:

  • Tüm yaşam süreleri için where $heap : $a$a
  • where $cm : $ro
  • where $x : $local tüm önceden tanımlanmış yaşam süreleri için geçerlidir. Kullanıcı tanımlı yaşam sürelerinin açıkça tanımlanmadığı sürece yerel ile ilişkisi yoktur.

Türlerde tanımlandığında yaşam süresi değişkenleri sabit veya kovaryan olabilir. Bunlar genel parametrelerle aynı söz dizimi kullanılarak ifade edilir:

// $this is covariant
// $a is invariant
ref struct S<out $this, $a> 

Tür tanımlarında $this yaşam süresi parametresi önceden tanımlı değildir, ancak tanımlandığı zaman bununla ilişkili birkaç kural vardır.

  • Yaşam süresi parametresi ilk sırada olmalıdır.
  • Birlikte değişken olmalıdır: out $this.
  • ref alanlarının ömrü $this'e dönüştürülebilir olmalıdır.
  • Tüm referans olmayan alanların $this süresi $heap veya $thisolmalıdır.

Başvurunun ömrü, başvuruya bir ömür bağımsız değişkeni sağlanarak ifade edilir. Örneğin, yığına atıfta bulunan bir ref, ref<$heap>olarak ifade edilir.

Modelde bir oluşturucu tanımlarken yöntem için new adı kullanılır. Hem döndürülen değer hem de oluşturucu bağımsız değişkenleri için bir parametre listesi olması gerekir. Bu, oluşturucu girişleri ile inşa edilen değer arasındaki ilişkiyi ifade etmek için gereklidir. Model Span<$a><$ro> yerine Span<$a> new<$ro> kullanır. Oluşturucuda yaşam süreleri de dahil olmak üzere this türü, tanımlı dönüş değeri olarak belirlenecektir.

Yaşam süresi için temel kurallar şu şekilde tanımlanır:

  • Tüm ömürler, tür argümanlarından önce gelen genel parametreler olarak ifade edilir. Bu, $heap ve $localdışında önceden tanımlanmış yaşam süreleri için geçerlidir.
  • T olmayan tüm türler ref struct örtük olarak T<$heap>ömrüne sahiptir. Bu örtük bir işlemdir, her örneğe int<$heap> yazmanız gerekmez.
  • refolarak tanımlanan bir ref<$l0> T<$l1, $l2, ... $ln> alanı için:
    • $l1'dan $ln'e kadar tüm yaşam süreleri sabit olmalıdır.
    • $l0 yaşam süresi $this'e dönüştürülebilir olmalıdır
  • refolarak tanımlanan bir ref<$a> T<$b, ...> için $b'nin $a'e dönüştürülebilir olması gereklidir.
  • Bir değişkenin ref'sının, aşağıdakiler tarafından tanımlanan bir ömrü vardır:
    • ref yerel, parametre, alan veya türü ref<$a> T olan bir döndürme için ömür $a'dur.
    • Tüm başvuru türleri ve başvuru türü alanları için $heap
    • Diğer her şey için $local
  • Temel tür dönüştürmesi yasal olduğunda atama veya geri dönüş yasaldır.
  • İfadelerin yaşam süreleri, atama ek açıklamaları kullanılarak açık hale getirilebilir:
    • (T<$a> expr) için değer ömrü açıkça $aT<...>
    • ref<$a> (T<$b>)expr değer ömrü $b için T<...> ve başvuru ömrü $a.

Yaşam kuralları amacıyla ref, dönüştürmeler amacıyla ifade türünün bir parçası olarak kabul edilir. Mantıksal olarak, ref<$a> T<...> kovaryant ve ref<$a, T<...>> invaryant olduğu durumda $a, T'e dönüştürülerek temsil edilir.

Şimdi C# söz dizimini temel alınan modelle eşlememize olanak sağlayan kuralları tanımlayalım.

Kısalık açısından, açık yaşam süresi parametreleri olmayan bir tür, türün tüm alanlarına tanımlanmış ve uygulanmış out $this varmış gibi ele alınır. ref alanı olan bir tür, açık yaşam süresi parametrelerini tanımlamalıdır.

Bu kurallar, tüm türler için T'ın scoped T'e atanabilmesi sabitini desteklemek amacıyla mevcuttur. T<$a, ...>'ye dönüştürülebilir olduğu bilinen tüm ömürler için T<$local, ...>, $local'e atanabilir. Ayrıca bu, yığındakilere yığından Span<T> atayabilme gibi diğer öğeleri destekler. Başvuru olmayan değerler için alanların farklı yaşam sürelerine sahip olduğu durumları dışlar; ancak bu, günümüzde C#'ın bir gerçeğidir. Bunun değiştirilmesi, eşlenmesi gereken C# kurallarında önemli bir değişiklik yapılmasını gerektirir.

Bir örnek yöntemi içindeki bir tür this için S<out $this, ...> türü örtük olarak aşağıdaki gibi tanımlanır:

  • Normal örnek yöntemi için: ref<$local> S<$cm, ...>
  • Örneğin, [UnscopedRef]ile açıklama ekleme yöntemi: ref<$ro> S<$cm, ...>

Açık bir this parametresinin olmamasından dolayı burada örtük kurallar devreye girer. Karmaşık örnekler ve tartışmalar için static bir yöntem olarak yazmayı ve this açık bir parametre yapmayı göz önünde bulundurun.

ref struct S<out $this>
{
    // Implicit this can make discussion confusing 
    void M<$ro, $cm>(ref<$ro> S<$cm> s) {  }

    // Rewrite as explicit this to simplify discussion
    static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}

C# yönteminin söz dizimi modelle aşağıdaki yollarla eşler:

  • ref parametrenin başvuru ömrü: $ro
  • ref struct türünden parametreler, $cm ömrüne sahiptir.
  • "referans dönüşlerinin referans ömrü $ro"
  • ref struct türünün dönüşleri $ro değer ömrüne sahiptir
  • scoped bir parametrede veya ref, başvuru ömrünü $local olacak şekilde değiştirir.

Burada modeli gösteren basit bir örneği inceleyelim:

ref int M1(ref int i) => ...

// Maps to the following. 

ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
    // okay: has ref lifetime $ro which is equal to $ro
    return ref i;

    // okay: has ref lifetime $heap which convertible $ro
    int[] array = new int[42];
    return ref array[0];

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

Şimdi ref structkullanarak aynı örneği inceleyelim:

ref struct S
{
    ref int Field;

    S(ref int f)
    {
        Field = ref f;
    }
}

S M2(ref int i, S span1, scoped S span2) => ...

// Maps to 

ref struct S<out $this>
{
    // Implicitly 
    ref<$this> int Field;

    S<$ro> new<$ro>(ref<$ro> int f)
    {
        Field = ref f;
    }
}

S<$ro> M2<$ro>(
    ref<$ro> int i,
    S<$ro> span1)
    S<$local> span2)
{
    // okay: types match exactly
    return span1;

    // error: has lifetime $local which has no conversion to $ro
    return span2;

    // okay: type S<$heap> has a conversion to S<$ro> because $heap has a
    // conversion to $ro and the first lifetime parameter of S<> is covariant
    return default(S<$heap>)

    // okay: the ref lifetime of ref $i is $ro so this is just an 
    // identity conversion
    S<$ro> local = new S<$ro>(ref $i);
    return local;

    int[] array = new int[42];
    // okay: S<$heap> is convertible to S<$ro>
    return new S<$heap>(ref<$heap> array[0]);

    // okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These 
    // are convertible.
    return new S<$ro>(ref<$heap> array[0]);

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

Şimdi bunun döngüsel kendi kendine atama sorununa nasıl yardımcı olduğunu görelim:

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        s.refField = ref s.field;
    }
}

// Maps to 

ref struct S<out $this>
{
    int field;
    ref<$this> int refField;

    static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
    {
        // error: the types work out here to ref<$cm> int = ref<$ro> int and that is 
        // illegal as $ro has no conversion to $cm (the relationship is the other direction)
        s.refField = ref<$ro> s.field;
    }
}

Şimdi bunun aptal yakalama parametresi sorununa nasıl yardımcı olduğunu görelim:

ref struct S
{
    ref int refField;

    void Use(ref int parameter)
    {
        // error: this needs to be an error else every call to this.Use(ref local) would fail 
        // because compiler would assume the `ref` was captured by ref.
        this.refField = ref parameter;
    }
}

// Maps to 

ref struct S<out $this>
{
    ref<$this> int refField;
    
    // Using static form of this method signature so the type of this is explicit. 
    static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
    {
        // error: the types here are:
        //  - refField is ref<$cm> int
        //  - ref parameter is ref<$ro> int
        // That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and 
        // hence this reassignment is illegal
        @this.refField = ref<$ro> parameter;
    }
}

Açık Sorunlar

Uyumluluk bozulmalarını önlemek için tasarımı değiştir

Bu tasarım, mevcut ref-safe-context kurallarımızla çeşitli uyumluluk kesintileri getiriyor. Kırılmaların minimal etkiye sahip olduğuna inanılsa da, hiçbir kırılma değişikliği yapmadan bir tasarıma büyük önem verilmiştir.

Bununla birlikte, uyumluluk koruma tasarımı bu tasarımdan çok daha karmaşıktı. Uyumluluk ref alanlarını korumak için, ref ve ref alana göre döndürme olanağı için farklı yaşam süreleri gerekir. Temelde, bir yöntemin tüm parametreleri için ref-field-safe-context izleme sağlamamız gerekir. Bunun tüm ifadeler için hesaplanması ve ref-safe-context bugün izlendiği her yerde neredeyse tüm değerlerde izlenmesi gerekir.

Ayrıca bu değerin ref-safe-contextile ilişkileri vardır. Örneğin, bir değerin ref alanı olarak döndürülmesi mantıklı değildir, ancak doğrudan refolarak döndürülemez. Bunun nedeni, ref alanlarının zaten ref tarafından kolaylıkla döndürülebilmesidir (ref durumu bir ref struct içinde, içeren değer döndürülemese bile ref tarafından döndürülebilir). Bu nedenle kurallar, bu değerlerin birbirine göre duyarlı olduğundan emin olmak için sürekli ayarlamaya ihtiyaç duyar.

Ayrıca, dilin üç farklı yolla döndürülebilecek ref parametreleri temsil etmesi için söz dizimine ihtiyacı olduğu anlamına gelir: ref alana göre, ref ve değere göre. Varsayılan olarak refile geri döndürülebilir. İleride, özellikle ref struct söz konusu olduğunda, dönüşün ref alanı veya reftarafından daha doğal olması beklenir. Bu, yeni API'lerin varsayılan olarak doğru çalışabilmesi için ek bir söz dizimi açıklamasına ihtiyaç duyacağı anlamına gelir. Bu istenmeyen bir durum.

Ancak bu uyumluluk değişiklikleri aşağıdaki özelliklere sahip yöntemleri etkiler:

  • Bir tane Span<T> veya ref struct
    • burada ref struct bir dönüş türü, ref veya out parametresidir
    • Ek bir in veya ref parametresi vardır (alıcı hariç)

Etkiyi anlamak için API'leri kategorilere ayırmak yararlı olur:

  1. Tüketicilerin ref'ın ref alanı olarak yakalanmasını hesaba katmasını istiyoruz. İlk örnek, Span(ref T value) oluşturucularıdır
  2. Tüketicilerin ref'ın ref alanı olarak yakalanmasının hesaba katılmasını istemeyin. Ancak bunlar iki kategoriye ayrılır
    1. Güvenli olmayan API'ler. Bunlar, Unsafe ve MemoryMarshal türleri içindeki API'lerdir ve bunların MemoryMarshal.CreateSpan en öne çıkanlardır. Bu API'ler ref güvenli olmayan bir şekilde yakalar, ancak bunların güvenli olmayan API'ler olduğu da bilinir.
    2. Güvenli API'ler. Bunlar, etkinlik amacıyla ref parametreleri alan, ancak aslında hiçbir yerde kaydedilmeyen API'lerdir. Örnekler küçük ama biri AsnDecoder.ReadEnumeratedBytes'dır.

Bu değişiklik öncelikli olarak yukarıdaki avantajlardan (1) yararlanır. Bunların ref alan ve ileriye dönük bir ref struct döndüren API'lerin çoğunu oluşturması beklenir. Bu değişiklikler, yaşam süresi kurallarının değişmesi nedeniyle var olan çağrı semantiğini bozarak (2.1) ve (2.2) değerlerini olumsuz etkiler.

Kategoride (2.1) yer alan API'ler ise büyük ölçüde ya Microsoft tarafından ya da ref alanlarından en çok fayda gören geliştiriciler (Tanner gibi kişiler) tarafından yazılıyor. Bu geliştirici sınıfının, ref alanlar sağlandığında mevcut semantiği korumak için birkaç açıklama ekleyerek C# 11'e yükseltme yapılırken uyumluluk vergisini kabul edebileceğini varsaymak mantıklıdır.

Kategorideki (2.2) API'ler en büyük sorundur. Bu tür API'lerin sayısı bilinmiyor ve bunların üçüncü taraf kodda daha sık /daha az sık olup olacağı belirsizdir. Beklenti, özellikle outüzerinde uyumluluk kırılmasını kabul edersek, onların çok az sayıda olması yönündedir. Şimdiye kadar yapılan aramalar, public yüzey alanında bunlardan çok az sayıda mevcut olduğunu ortaya çıkardı. Bu, anlamsal analiz gerektirdiği için aranacak zor bir desendir. Bu değişikliği almadan önce, az sayıda bilinen durumu etkileyen varsayımları doğrulamak için araç tabanlı bir yaklaşım gerekir.

Her iki kategorideki (2) durum için düzeltmek oldukça basittir. Yakalanabilir olarak değerlendirilmek istenmeyen ref parametreleri, scoped'i ref'ye eklemelidir. (2.1) içinde bu büyük olasılıkla geliştiriciyi Unsafe veya MemoryMarshal kullanmaya zorlar, ancak güvenli olmayan stil API'leri için bu beklenir.

İdeal olarak dil, bir API sessizce sorunlu davranışa düştüğünde uyarı vererek sessiz hataya neden olan değişikliklerin etkisini azaltabilir. Bu, hem refalıp hem ref struct döndüren, ancak refiçinde ref struct yakalamayan bir yöntem olacaktır. Derleyici bu durumda geliştiricilere bunun yerine refscoped ref olarak ek açıklama eklemeleri gerektiğini bildiren bir tanılama verebilir.

Karar Bu tasarım elde edilebilir ancak sonuçta elde edilen özelliğin, uyumluluk molası verme kararı alındığı noktaya kadar kullanılması daha zordur.

Karar Derleyici, bir yöntem ölçütleri karşıladığında ancak ref parametresini ref alanı olarak yakalamadığında bir uyarı sağlar. Bu, müşterileri yükseltme konusunda oluşturdukları olası sorunlar konusunda uygun şekilde uyarmalıdır

Anahtar sözcükler ile öznitelikler karşılaştırması

Bu tasarım, yeni yaşam süresi kurallarına açıklama eklemek için öznitelikleri kullanmayı çağırır. Bu, bağlamsal anahtar sözcüklerle de aynı şekilde kolayca yapılabilirdi. Örneğin [DoesNotEscape]scopedile eşlenebilir. Ancak, bağlamsal anahtar sözcükler bile dahil olmak üzere genellikle çok yüksek bir çıtayı karşılamalıdır. Değerli dil alanını kaplarlar ve dilin daha belirgin parçalarıdır. Bu özellik değerli olsa da C# geliştiricilerinin azınlığa hizmet edecektir.

Yüzeyde anahtar sözcükleri kullanmamayı tercih ediyor gibi görünse de dikkate alınması gereken iki önemli nokta vardır:

  1. Ek açıklamalar program semantiğini etkiler. Program semantiğini özniteliklerin etkilemesi, C# dilinin temkinli davrandığı bir konudur ve bunun, dilin bu adımı atmasını gerektirecek bir özellik olup olmadığı belirsizdir.
  2. Geliştiricilerin büyük olasılıkla bu özelliği kullanması, işlev işaretçilerini kullanan geliştirici kümesiyle güçlü bir şekilde kesişmektedir. Bu özellik, geliştiricilerin yalnızca azınlığı tarafından kullanılıyor olsa da yeni bir söz dizimini haklı çıkarıyordu ve bu kararın hâlâ yerinde olduğu düşünülüyor.

Birlikte ele alındığında bu, söz dizimlerinin dikkate alınması gerektiği anlamına gelir.

Söz diziminin kaba bir taslağı şöyle olabilir:

  • [RefDoesNotEscape] scoped ref'e haritalanır
  • [DoesNotEscape] scoped'e haritalanır
  • [RefDoesEscape] unscoped'e haritalanır

Kararscoped ve scoped refiçin söz dizimini kullanın; unscopediçin özniteliği kullanın.

Sabit arabellek yerel ayarlarına izin ver

Bu tasarım, herhangi bir türü destekleyebilecek güvenli fixed arabellekleri sağlar. Buradaki olası uzantılardan biri, bu tür fixed arabelleklerinin yerel değişkenler olarak bildirilmesine izin vermektir. Bu, birçok mevcut stackalloc işleminin fixed arabelleğiyle değiştirilmesine olanak tanır. Bu, ayrıca, stackalloc yönetilmeyen öğe türleriyle sınırlı olduğu için yığın tarzı tahsisatlara sahip olabileceğimiz senaryolar kümesini genişletir, ancak fixed arabellekler bu kısıtlamaya tabi değildir.

class FixedBufferLocals
{
    void Example()
    {
        Span<int> span = stackalloc int[42];
        int buffer[42];
    }
}

Bu durumu bir arada tutar, ancak yerel değişkenler için söz dizimini biraz genişletmemizi gerektirir. Bu ekstra karmaşıklığa değip değmediği net değil. Şimdilik hayır'a karar verebilir ve yeterli ihtiyaç gösteriliyorsa daha sonra geri getirebiliriz.

Bunun yararlı olacağı yer örneği: https://github.com/dotnet/runtime/pull/34149

Karar şimdilik bu işlemden uzak tutun

Modreqs kullanmak mı yoksa kullanmamak mı?

Yeni yaşam süresi öznitelikleriyle işaretlenmiş yöntemlerin yayım sürecinde modreq'a çevrilip çevrilmemesi gerektiğine dair bir karar verilmelidir. Bu yaklaşım uygulanırsa ek açıklamalar ile modreq arasında etkili bir 1:1 eşlemesi olacaktır.

modreq eklemenin mantığı, özniteliklerin başvuru güvenli bağlam kurallarının semantiğini değiştirmesidir. Yalnızca bu semantiği anlayan diller söz konusu yöntemleri çağırmalıdır. Ayrıca, OHI senaryolarına uygulandığında, yaşam süreleri tüm türetilmiş yöntemlerin uygulaması gereken bir sözleşme haline gelir. Ek açıklamaların modreq olmadan var olması, çakışan yaşam süresi ek açıklamalarına sahip virtual yöntem zincirlerinin yüklendiği durumlara yol açabilir (bu, yalnızca virtual zincirinin bir kısmı derlenip diğer kısmı derlenmediğinde meydana gelebilir).

İlk referans güvenli bağlam çalışması modreq'u kullanmadı, bunun yerine anlamak için dillere ve yazılım çerçevesine güvendi. Aynı zamanda, başvuru güvenli bağlam kurallarına katkıda bulunan tüm öğeler yöntem imzasının güçlü bir parçasıdır: ref, in, ref structvb. Bu nedenle, bir yöntemin mevcut kurallarında yapılan herhangi bir değişiklik imzada ikili bir değişiklikle sonuç olur. Yeni ömür adlandırmalarına aynı etkiyi sağlamak için modreq zorlaması gerekir.

Sorun, bunun aşırıya kaçıp kaçmadığıdır. [DoesNotEscape]'ın bir parametreye eklenmesi gibi imzaları daha esnek hale getirmenin, gerçekten ikili uyumluluk değişikliğine neden olma gibi olumsuz bir etkisi vardır. Bu da BCL gibi çerçevelerin zaman içinde bu tür imzaları gevşetemeyeceği anlamına gelir. Dilin in parametreleriyle benimsediği bir yaklaşımı benimseyerek ve yalnızca sanal konumlarda modreq uygulayarak bir dereceye kadar hafifletilebilir.

Karar Meta verilerde modreq kullanmayın. out ile ref arasındaki fark modreq değildir, ancak şu anda farklı referans güvenli bağlam değerlerine sahiptir. Burada modreq kurallarını kısmen uygulamanın gerçek bir yararı yoktur.

Çok boyutlu sabit arabelleklere izin ver

fixed arabelleklerin tasarımı çok boyutlu stil dizileri içerecek şekilde genişletilmeli mi? Temelde aşağıdaki gibi bildirimlere imkan tanıma:

struct Dimensions
{
    int array[42, 13];
}

Karar Şimdilik izin verme

Kapsamı ihlal etme

Çalışma zamanı deposunda, ref parametreleri ref alanları olarak yakalayan genel olmayan birkaç API vardır. Sonuçta elde edilen değerin ömrü izlenmediğinden bunlar güvenli değildir. Örneğin, Span<T>(ref T value, int length) oluşturucu.

Bu API'lerin çoğu, yalnızca C# 11'e güncellenerek sağlanabilecek olan, geri dönüş değerlerinde uygun yaşam süresi takibi yapmayı tercih edecektir. Ancak, asıl niyetleri güvensiz kalmak olduğundan, bazıları, dönüş değerini izlemeyen mevcut semantiğini korumak isteyecektir. En önemli örnekler MemoryMarshal.CreateSpan ve MemoryMarshal.CreateReadOnlySpan. Bu, parametreleri scopedolarak işaretleyerek elde edilir.

Bu, çalışma zamanının scoped'ı bir parametreden güvenli olmayan bir şekilde kaldırmak için yerleşik bir desene ihtiyacı olduğu anlamına gelir.

  1. Unsafe.AsRef<T>(in T value) scoped in T valueolarak değiştirerek mevcut amacını genişletebilir. Bu, hem in hem de scoped parametrelerden kaldırmasına olanak sağlar. Daha sonra "referans güvenliğini kaldırma" evrensel yöntemi haline gelir.
  2. Amacının tamamı scopedkaldırmak olan yeni bir yöntem tanıtın: ref T Unsafe.AsUnscoped<T>(scoped in T value). Bu, aynı zamanda in'ı da kaldırır çünkü eğer kaldırmasaydı, çağıranlar "referans güvenliğini kaldırmak" için hâlâ yöntem çağrılarının bir kombinasyonuna ihtiyaç duyardı ve bu durumda mevcut çözüm büyük olasılıkla yeterlidir.

Bunun kapsamı varsayılan olarak kaldırılsın mı?

Tasarım, varsayılan olarak scoped olan iki konuma sahiptir.

  • this eşittir scoped ref
  • out eşittir scoped ref

out ile ilgili karar, ref alanların uyumluluk yükünü önemli ölçüde azaltmaktır ve aynı zamanda daha doğal bir varsayılandır. Geliştiricilerin, eğer out verilirse verinin yalnızca dışa doğru aktığını düşünmelerine olanak tanır. Oysa ref söz konusuysa, kurallar her iki yönde de veri akışını dikkate almalıdır. Bu, önemli geliştirici karışıklığına neden olur.

this kararı, istenmeyen bir durumdur çünkü bu karar, bir struct'in reftarafından bir alan döndüremeyeceği anlamına gelir. Bu, yüksek performansa sahip geliştiriciler için önemli bir senaryodur ve bu senaryo için temel olarak [UnscopedRef] özniteliği eklenmiştir.

Anahtar sözcüklerin yüksek bir standardı vardır ve bunu tek bir senaryo için eklemek şüphelidir. Şu düşünceye varılmıştır: Acaba this'ı varsayılan olarak ref yapıp scoped refolmaktan çıkartarak bu anahtar sözcükten tamamen kaçınabilir miyiz? this'ın scoped ref olması gereken tüm üyeler, bunu sağlamak için yöntemi scoped ile işaretleyebilir (bugün bir readonly oluşturmak için bir yöntem readonly ref olarak işaretlenebilir).

Normal bir struct'da bu, çoğunlukla olumlu bir değişikliktir çünkü yalnızca bir üyenin ref dönüşü olduğunda uyumluluk sorunları ortaya çıkar. Bu yöntemlerden çok azı vardır ve bir araç bunları tespit edebilir ve hızla üyelerine dönüştürebilir.

Bir ref struct'de bu değişiklik, önemli ölçüde daha fazla uyumluluk sorununa neden olur. Aşağıdakileri göz önünde bulundurun:

ref struct Sneaky
{
    int Field;
    ref int RefField;

    public void SelfAssign()
    {
        // This pattern of ref reassign to fields on this inside instance methods would now
        // completely legal.
        RefField = ref Field;
    }

    static Sneaky UseExample()
    {
        Sneaky local = default;

        // Error: this is illegal, and must be illegal, by our existing rules as the 
        // ref-safe-context of local is now an input into method arguments must match. 
        local.SelfAssign();

        // This would be dangerous as local now has a dangerous `ref` but the above 
        // prevents us from getting here.
        return local;
    }
}

Temel olarak, değiştirilebilirref struct yerellerdeki tüm örnek yöntemi çağrılarının, yerel daha sonra scopedolarak işaretlenmedikçe yasadışı olacağı anlamına gelir. Kuralların, alanların this'deki diğer alanlara yeniden atandığı durumu göz önünde bulundurması gerekir. readonly ref struct'da bu sorun yoktur çünkü readonly doğası başvurunun yeniden atanmasını önler. Yine de bu, neredeyse tüm mevcut değiştirilebilir ref struct'ları etkilediğinden, geri uyumluluğu önemli ölçüde bozan bir değişiklik olacaktır.

Ancak readonly ref struct, refiçin ref struct alanlara sahip olmaya genişlettiğimizde yine de sorunlu olur. Yakalamayı ref alanının değerine taşıyarak aynı temel sorunun çözümüne olanak tanır:

readonly ref struct ReadOnlySneaky
{
    readonly int Field;
    readonly ref ReadOnlySpan<int> Span;

    public void SelfAssign()
    {
        // Instance method captures a ref to itself
        Span = new ReadOnlySpan<int>(ref Field, 1);
    }
}

this veya üye türüne göre struct'ın farklı varsayılanlara sahip olması fikri üzerine bazı düşünceler düşünüldü. Örneğin:

  • this olarak ref: struct, readonly ref struct veya readonly member
  • this olarak scoped ref: ref struct veya readonly ref struct ile ref alanına ref struct

Bu, uyumluluk sorunlarını en aza indirir ve esnekliği en üst düzeye çıkarır, ancak müşteriler için durumu karmaşıklaştırma pahasına. Gelecekteki özelliklerden biri olan güvenli fixed arabellekleri, değiştirilebilir bir ref struct'in, yalnızca bu tasarımla çalışmayan alanlar için ref döndürmesi gerektiğinden ve bu nedenle scoped ref kategorisine gireceğinden dolayı sorunu tam olarak çözmez.

Kararthisscoped refolarak tutun. Bu, yukarıdaki gizli örneklerin derleyici hataları ürettiği anlamına gelir.

ref alanlarını ref yapısına dönüştür

Bu özellik, bir ref alanının bir ref struct'e başvurabilmesi sayesinde yeni bir başvuru güvenli bağlam kuralları kümesi açar. ByReference<T>'nin bu genel doğası, şimdiye kadar çalışma zamanının böyle bir yapıya sahip olmaması anlamına geliyordu. Sonuç olarak tüm kurallarımız bunun mümkün olmadığı varsayımı ile yazılmıştır. ref alanı özelliği büyük ölçüde yeni kurallar oluşturmakla değil, sistemimizdeki mevcut kuralları koordine etmeyle ilgilidir. ref alanlarının ref struct'e izin vermesi, dikkate alınması gereken birkaç yeni senaryo olduğundan yeni kuralları kodlamamızı gerektirir.

Birincisi, bir readonly ref artık ref durumunu depolayabilme özelliğine sahip olmasıdır. Örneğin:

readonly ref struct Container
{
    readonly ref Span<int> Span;

    void Store(Span<int> span)
    {
        Span = span;
    }
}

Bu, yöntem bağımsız değişkenlerini düşünürken, kuralların eşleşmesi gerektiği anlamına gelir; çünkü readonly ref T, Tiçin bir ref alanına sahip olduğunda, ref struct potansiyel bir yöntem çıktısıdır.

İkinci sorun, dilin yeni bir güvenli bağlam türünü dikkate alması gerektiğidir: ref-field-safe-context. Geçişli olarak bir ref struct alanı içeren tüm ref, ref alanlarındaki değerleri temsil eden başka bir çıkış kapsamına sahiptir. Birden çok ref alanı söz konusu olduğunda, bunlar tek bir değer olarak toplu olarak izlenebilir. Bunun için parametrelerin varsayılan değeri çağıran bağlam'dir.

ref struct Nested
{
    ref Span<int> Span;
}

Span<int> M(ref Nested nested) => nested.Span;

Bu değer kapsayıcının güvenli bağlamı ile ilgili değildir; kapsayıcının bağlamı küçüldükçe, alan değerlerinin ref üzerinde hiçbir etkisi yoktur. Ayrıca ref-field-safe-context, kapsayıcının güvenli bağlamı'dan asla daha küçük olamaz.

ref struct Nested
{
    ref Span<int> Span;
}

void M(ref Nested nested)
{
    scoped ref Nested refLocal = ref nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is illegal
    refLocal.Span = stackalloc int[42];

    scoped Nested valLocal = nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is still illegal
    valLocal.Span = stackalloc int[42];
}

Bu ref-field-safe-context temelde her zaman mevcut olmuştur. Şimdiye kadar ref alanlar yalnızca normal struct'i işaret edebildiğinden, bu durum kolayca çağrı bağlamınaindirgenmiştir. Mevcut kurallarımızın, bu yeni refdikkate alınarak ref struct alanlarını desteklemek için güncellenmesi gerekiyor.

Üçüncü olarak, değerler için ref-field-context ’ı ihlal etmemek adına ref yeniden atama kurallarının güncellenmesi gerekir. Esasen, x.e1 = ref e2 türünün bir e1 olduğu ref struct için, ref-field-safe-context ile eşdeğer olmalıdır.

Bu sorunlar çok çözülebilir. Derleyici ekibi bu kuralların birkaç sürümünün taslağını çizmiştir ve bunlar büyük ölçüde mevcut analizimizden çıkar. Sorun şu ki, bu tür kuralların doğruluğunu ve kullanılabilirliğini kanıtlamaya yardımcı olan bir tüketici kodu yoktur. Bu, yanlış varsayılanları seçeceğimiz ve çalışma zamanını bu avantajı kullandığında kullanılabilirlik açısından köşeye sıkıştıracağımız korkusu nedeniyle destek eklemekte çok tereddütlü olmamıza neden oluyor. .NET 8 büyük olasılıkla bizi allow T: ref struct ve Span<Span<T>>ile bu yönde ittiği için bu endişe özellikle güçlüdür. Kurallar, tüketim koduyla birlikte yapılırsa daha iyi yazılır.

Karar Gecikmesi, ref alanın .NET 8'e kadar ref struct edilmesine izin vermeyi geciktiriyor ve bu süreçte söz konusu senaryoların etrafındaki kuralları yönlendirmeye yardımcı olacak senaryolar geliştiriliyor. Bu işlem .NET 9 itibarıyla uygulanmadı

C# 11.0 ne olacak?

Bu belgede özetlenen özelliklerin tek bir geçişte uygulanması gerekmez. Bunun yerine, aşağıdaki demetlerde çeşitli dil sürümleri arasında aşamalar halinde uygulanabilirler:

  1. ref alanları ve scoped
  2. [UnscopedRef]
  3. ref alanlarından ref struct alanlarına
  4. Gün batımı kısıtlanmış türleri
  5. sabit boyutlu arabellekler

Hangi sürümde nelerin uygulandığı sadece bir kapsam belirleme çalışmasıdır.

Karar Yalnızca (1) ve (2) C# 11.0'u oluşturdu. Gerisi C# dilinin gelecek sürümlerinde dikkate alınacaktır.

Gelecekte Dikkat Edilmesi Gerekenler

Gelişmiş yaşam süresi ek açıklamaları

Bu teklifteki yaşam süresi ek açıklamaları, geliştiricilerin değerlerin varsayılan kaçış/dışarıda kalış davranışını değiştirmesine izin vermeleri konusunda sınırlıdır. Bu, modelimize güçlü bir esneklik getirir, ancak ifade edilebilecek ilişki kümesini kökten değiştirmez. Özünde C# modeli hala temelde ikili bir yapıdadır: Bir değer döndürülebilir mi, yoksa döndürülemez mi?

Bu, sınırlı yaşam süresi ilişkilerinin anlaşılmasını sağlar. Örneğin, bir yöntemden döndürülemez bir değer, bir yöntemden döndürülebilecek bir değerden daha küçük bir yaşam süresine sahiptir. Ancak bir yöntemden döndürülebilecek değerler arasındaki yaşam süresi ilişkisini açıklamanın hiçbir yolu yoktur. Özellikle, bir değerin bir kez oluşturulduktan sonra diğerinden daha uzun bir ömrü olduğunu söylemenin hiçbir yolu yoktur. Her ikisi de bir yöntemden döndürülebilir. Yaşam döngüsü evrimimizdeki bir sonraki adım, bu tür ilişkilerin açıklanması olacaktır.

Rust gibi diğer yöntemler bu tür bir ilişkinin ifade edilmesine olanak sağlar ve bu nedenle daha karmaşık scoped stil işlemleri uygulayabilir. Böyle bir özellik dahil edilmişse dilimiz de benzer şekilde yararlı olabilir. Şimdilik bunu yapmak için motive edici bir baskı yok, ancak gelecekte olursa, scoped modelimiz bunu oldukça rahat bir şekilde içerecek şekilde genişletilebilir.

Söz dizimine bir şablon tarzı bağımsız değişkeni eklenerek her scoped'a adlandırılmış bir yaşam süresi atanabilir. Örneğin scoped<'a>, yaşam süresi 'aolan bir değerdir. Daha sonra where gibi kısıtlamalar bu yaşam süreleri arasındaki ilişkileri açıklamak için kullanılabilir.

void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
  where 'b >= 'a
{
    s.Span = span;
}

Bu yöntem 'a ve 'b olmak üzere iki yaşam süresi ve aralarındaki ilişkiyi tanımlar, özellikle de 'b'nin 'a'ten büyük olduğunu belirtir. Bu, çağrı noktasının, günümüzdeki daha genel kurallara kıyasla, değerlerin yöntemlere nasıl güvenli bir şekilde geçirilebileceği konusunda daha ayrıntılı kurallara sahip olmasını sağlar.

Sorun

Aşağıdaki sorunların tümü bu teklifle ilgilidir:

Teklif

Aşağıdaki teklifler bu teklifle ilgilidir:

Mevcut örnekler

Utf8JsonReader

Bu özel kod parçacığı, bir Span<T>üzerindeki örnek bir yönteme yığından tahsis edilebilen bir ref struct geçirirken ortaya çıkan sorunlar nedeniyle "unsafe" anahtar kelimesine ihtiyaç duyar. Bu parametre yakalanmamış olsa bile, dilin olduğunu varsayması gerekir ve bu nedenle burada gerekli olmayan bir şekilde sürtünmeye neden olur.

Utf8JsonWriter

Bu kod parçacığı, verinin öğelerini işleyerek bir parametreyi değiştirmek istiyor. Kaçış verileri, verimlilik amacıyla yığında bellekte tahsis edilebilir. Parametresinden kaçılmamasına rağmen, derleyici ona bir parametre olduğundan kapsayan yöntemin dışında bir güvenli bağlam atar. Bu, yığın ayırmayı kullanmak için uygulamanın verilerden kaçtıktan sonra parametreye geri atamak için unsafe kullanması gerektiği anlamına gelir.

Eğlenceli Örnekler

ReadOnlySpan<T>

public readonly ref struct ReadOnlySpan<T>
{
    readonly ref readonly T _value;
    readonly int _length;

    public ReadOnlySpan(in T value)
    {
        _value = ref value;
        _length = 1;
    }
}

Frugal listesi

struct FrugalList<T>
{
    private T _item0;
    private T _item1;
    private T _item2;

    public int Count = 3;

    public FrugalList(){}

    public ref T this[int index]
    {
        [UnscopedRef] get
        {
            switch (index)
            {
                case 0: return ref _item0;
                case 1: return ref _item1;
                case 2: return ref _item2;
                default: throw null;
            }
        }
    }
}

Örnekler ve Notlar

Aşağıda kuralların nasıl ve neden çalıştıklarını gösteren bir dizi örnek verilmiştir. Tehlikeli davranışları ve kuralların bunların oluşmasını nasıl engelleyişini gösteren birkaç örnek de dahildir. Teklifte ayarlamalar yaparken bunları mutlaka göz önünde bulundurmak gerekir.

Yeniden atama ve arama siteleri

başvuru yeniden atama ve yöntemi çağırma birlikte nasıl çalıştığını gösterme.

ref struct RS
{
    ref int _refField;

    public ref int Prop => ref _refField;

    public RS(int[] array)
    {
        _refField = ref array[0];
    }

    public RS(ref int i)
    {
        _refField = ref i;
    }

    public RS CreateRS() => ...;

    public ref int M1(RS rs)
    {
        // The call site arguments for Prop contribute here:
        //   - `rs` contributes no ref-safe-context as the corresponding parameter, 
        //      which is `this`, is `scoped ref`
        //   - `rs` contribute safe-context of *caller-context*
        // 
        // This is an lvalue invocation and the arguments contribute only safe-context 
        // values of *caller-context*. That means `local1` has ref-safe-context of 
        // *caller-context*
        ref int local1 = ref rs.Prop;

        // Okay: this is legal because `local` has ref-safe-context of *caller-context*
        return ref local1;

        // The arguments contribute here:
        //   - `this` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `this` contributes safe-context of *caller-context*
        //
        // This is an rvalue invocation and following those rules the safe-context of 
        // `local2` will be *caller-context*
        RS local2 = CreateRS();

        // Okay: this follows the same analysis as `ref rs.Prop` above
        return ref local2.Prop;

        // The arguments contribute here:
        //   - `local3` contributes ref-safe-context of *function-member*
        //   - `local3` contributes safe-context of *caller-context*
        // 
        // This is an rvalue invocation which returns a `ref struct` and following those 
        // rules the safe-context of `local4` will be *function-member*
        int local3 = 42;
        var local4 = new RS(ref local3);

        // Error: 
        // The arguments contribute here:
        //   - `local4` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `local4` contributes safe-context of *function-member*
        // 
        // This is an lvalue invocation and following those rules the ref-safe-context 
        // of the return is *function-member*
        return ref local4.Prop;
    }
}

Yeniden atama ve güvenli olmayan kaçışlar

başvuru yeniden atama kuralları aşağıdaki satırın nedeni ilk bakışta belirgin olmayabilir:

e1, ile aynı e2 sahip olmalıdır

Bunun nedeni, ref konumların işaret ettiği değerlerin ömrünün sabit olmasıdır. Dolaylılık, burada her türlü varyansa izin vermemizi engelliyor, hatta daha dar yaşam süreleri için bile. Daraltmaya izin veriliyorsa aşağıdaki güvenli olmayan kodu açar:

void Example(ref Span<int> p)
{
    Span<int> local = stackalloc int[42];
    ref Span<int> refLocal = ref local;

    // Error:
    // The safe-context of refLocal is narrower than p. For a non-ref reassignment 
    // this would be allowed as its safe to assign wider lifetimes to narrower ones.
    // In the case of ref reassignment though this rule prevents it as the 
    // safe-context values are different.
    refLocal = ref p;

    // If it were allowed this would be legal as the safe-context of refLocal
    // is *caller-context* and that is satisfied by stackalloc. At the same time
    // it would be assigning through p and escaping the stackalloc to the calling
    // method
    // 
    // This is equivalent of saying p = stackalloc int[13]!!! 
    refLocal = stackalloc int[13];
}

ref olmayan bir ref struct için, değerlerin tümü aynı güvenli bağlamsahip olduğundan bu kural kolayca karşılanır. Bu kural gerçekten yalnızca değer bir ref structolduğunda devreye girer.

Bu ref davranışı, ref alanlarına ref structizin verebileceğimiz bir gelecekte de önemli olacaktır.

kapsamı belirlenmiş yereller

Yerellerde scoped kullanımı, yerel ayarlara farklı güvenli bağlam sahip değerleri koşullu olarak atayan kod desenleri için özellikle yararlı olacaktır. Bu, kodun artık = stackalloc byte[0] tanımlamak için gibi başlatma püf noktalarına bel bağlamasına gerek olmadığı, bunun yerine artık basitçe scopedkullanabileceği anlamına gelir.

// Old way 
// Span<byte> span = stackalloc byte[0];
// New way 
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
    span = stackalloc byte[len];
}
else
{
    span = new byte[len];
}

Bu düzen düşük düzeyli kodda sık sık ortaya çıkar. İlgili ref structSpan<T> yukarıdaki püf noktası kullanılabilir. Ancak diğer ref struct türleri için geçerli değildir ve düşük seviyeli kodun, ömrü doğru bir şekilde belirleyememe nedeniyle unsafe kullanması gerekebilir.

kapsamlı parametre değerleri

Düşük düzeyli kodda yinelenen sürtüşmelerden biri, parametreler için varsayılan kaçışın izin verilebilir olmasıdır. çağıran bağlamıgüvenli bağlam . Bu, bir bütün olarak .NET'in kodlama desenleriyle sıralandığından mantıklı bir varsayılan değerdir. Düşük düzeyli kodda ref struct'nın daha sık kullanılmasına rağmen bu varsayılan, referans güvenli bağlam kurallarının diğer bölümleriyle sürtüşmelere neden olabilir.

Ana sürtünme noktası, yöntemi bağımsız değişkenlerinin kuralıyla uyuşması gerektiği için meydana gelir. Bu kural, en yaygın olarak en az bir parametrenin de ref structolduğu ref struct üzerindeki örnek yöntemlerle devreye girer. Bu, düşük düzeyli kodda ref struct türlerinin yöntemlerindeki Span<T> parametrelerini sıkça kullandığı yaygın bir desendir. Örneğin, ref struct kullanarak arabellekleri geçiren herhangi bir yazım stili Span<T>'da gerçekleşir.

Aşağıdaki gibi senaryoları önlemek için bu kural vardır:

ref struct RS
{
    Span<int> _field;
    void Set(Span<int> p)
    {
        _field = p;
    }

    static void DangerousCode(ref RS p)
    {
        Span<int> span = stackalloc int[] { 42 };

        // Error: if allowed this would let the method return a reference to 
        // the stack
        p.Set(span);
    }
}

Temelde bu kural mevcut çünkü dil, bir yönteme yönelik tüm girişlerin izin verilen en yüksek güvenli bağlamkaçtığını varsaymalıdır. Alıcılar dahil olmak üzere ref veya out parametreleri olduğunda, girişlerin bu ref değerlerinin alanlarına dönüşmesi mümkündür (yukarıdaki RS.Set'te olduğu gibi).

Uygulamada, birçok yöntem ref struct'ı parametre olarak iletir, ancak bunlar hiçbir zaman sonucu etkilemeyi amaçlamaz. Yalnızca geçerli yöntem içinde kullanılan bir değerdir. Örneğin:

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Error: The safe-context of `span` is function-member 
        // while `reader` is outside function-member hence this fails
        // by the above rule.
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

Bu düşük düzeyli kodu geçici olarak çözmek için derleyiciye unsafeömrü hakkında yalan söylemeleri için ref struct püf noktalarına başvuracaktır. Bu, yüksek performanslı kod yazmaya devam ederken ref struct'i önlemenin bir yolu olması gerektiği için unsafe'ın değer önerisini önemli ölçüde azaltır.

scoped, güncellenmiş ref structkuralıyla eşleşmesi gerektiğinden, parametrelerini yöntemden döndürülmek üzere değerlendirme dışı bırakan etkili bir araçtır. Çağrı sitelerini daha esnek hale getirmek için tüketilen ancak hiç döndürülmeyen bir ref struct parametresi scoped olarak etiketlenebilir.

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(scoped ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Okay: the compiler never considers `span` as capturable here hence it doesn't
        // contribute to the method arguments must match rule
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

Karmaşık başvuru atamalarının salt okunur mutasyona uğramasını önleme

Bir ref, bir oluşturucu veya readonly üyedeki init alanına alındığında türü refdeğil, ref readonly olur. Bu, aşağıdaki gibi kodlara izin veren uzun süredir devam eden bir davranıştır:

struct S
{
    readonly int i; 

    public S(string s)
    {
        M(ref i);
    }

    static void M(ref int i) { }
}

Ancak bu tür bir ref aynı türdeki bir ref alanında depolanabiliyorsa bu durum olası bir sorun oluşturur. Örnek üyesinden bir readonly struct doğrudan mutasyona izin verir:

readonly ref struct S
{ 
    readonly int i; 
    readonly ref int r; 
    public S()
    {
        i = 0;
        // Error: `i` has a narrower scope than `r`
        r = ref i;
    }

    public void Oops()
    {
        r++;
    }
}

Teklif, referans güvenli bağlam kurallarını ihlal ettiği için bunu önler. Aşağıdakileri göz önünde bulundurun:

  • this'dir ve güvenli-bağlam, işlev üyesi ve çağırıcı-bağlam'dir. Bunların ikisi de this üyedeki struct için standarttır.
  • 'nin ref-güvenli-bağlamı,fonksiyon-üyesidir. Bu, alan ömrü kurallarına göre'den kaynaklanıyor. Özellikle kural 4.

Bu noktada r = ref i satırı, atıf yeniden atama kurallarıtarafından geçersizdir.

Bu kurallar bu davranışı önlemek için tasarlanmamıştır, ancak bunu bir yan etki olarak yapar. Bu gibi senaryoların etkisini değerlendirmek için gelecekteki tüm kural güncelleştirmelerinde bunu göz önünde bulundurmak önemlidir.

Aptal döngüsel atama

Bu tasarımın zorlandığı bir yön, bir ref'in bir yöntemden ne kadar serbestçe döndürülebileceğidir. Geliştiricilerin çoğunun sezgisel olarak beklediği şey, tüm ref'nun normal değerler kadar serbestçe döndürülmesini sağlamaktır. Ancak, referans güvenliğini hesaplarken derleyicinin dikkate alması gereken patolojik senaryolara olanak tanır. Aşağıdakileri göz önünde bulundurun:

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        // Error: s.field can only escape the current method through a return statement
        s.refField = ref s.field;
    }
}

Bu, geliştiricilerin kullanmasını beklediğimiz bir kod düzeni değildir. Ancak bir ref, bir değerle aynı yaşam süresine sahip olarak döndürülebildiğinde, kurallar kapsamında yasaldır. Derleyici, bir yöntem çağrısını değerlendirirken tüm yasal durumları dikkate almalıdır ve bu da bu TÜR API'lerin etkili bir şekilde kullanılamaz olmasına neden olur.

void M(ref S s)
{
    ...
}

void Usage()
{
    // safe-context to caller-context
    S local = default; 

    // Error: compiler is forced to assume the worst and concludes a self assignment
    // is possible here and must issue an error.
    M(ref local);
}

Bu API'leri kullanılabilir hale getirmek için derleyici, ref parametresi için ref ömrünün, ilişkili parametre değerindeki başvuruların ömründen daha kısa olmasını garanti eder. Bu, yalnızca dönüş ve çağıran bağlamolması için başvuru-güvenli bağlam sahip olmanın mantığıdır. Bu, yaşam süreleri arasındaki fark nedeniyle döngüsel atamayı önler.

[UnscopedRef] , çağıran bağlamı değerleri için refref struct yükseltir ve bu nedenle döngüsel atamaya izin verir ve çağrı zincirini [UnscopedRef] viral kullanımını zorlar:

S F()
{
    S local = new();
    // Error: self assignment possible inside `S.M`.
    S.M(ref local);
    return local;
}

ref struct S
{
    int field;
    ref int refField;

    public static void M([UnscopedRef] ref S s)
    {
        // Allowed: s has both safe-context and ref-safe-context of caller-context
        s.refField = ref s.field;
    }
}

Benzer şekilde , parametrenin hem güvenli bağlam hem de yalnızca dönüşref-safe-context döngüsel atamaya izin verir.

Tür [UnscopedRef] ref bir olmadığında, ref struct'ye yükseltmek faydalıdır (kuralları basit tutmak istediğimizden, başvuruları referans olmayan yapılardan ayırt etmeyeceklerini unutmayın):

int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2

static S F([UnscopedRef] ref int x)
{
    S local = new();
    local.M(ref x);
    return local;
}

ref struct S
{
    public ref int RefField;

    public void M([UnscopedRef] ref int data)
    {
        RefField = ref data;
    }
}

Gelişmiş ek açıklamalar açısından [UnscopedRef] tasarımı aşağıdakileri oluşturur:

ref struct S { }

// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)

// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
  where 'b >= 'a

readonly, başvuru alanları aracılığıyla derinlemesine yapılamaz

Aşağıdaki kod örneğini göz önünde bulundurun:

ref struct S
{
    ref int Field;

    readonly void Method()
    {
        // Legal or illegal?
        Field = 42;
    }
}

Bir vakumda ref örneklerde readonly alanları için kurallar tasarlanırken, kurallar geçerli olarak yukarıdakilerin yasal veya yasa dışı olması için tasarlanabilir. Temelde readonly, bir ref alanında derinlemesine işleyebilir veya yalnızca ref'ye uygulanabilir. Yalnızca ref uygulanması, referansın yeniden atanmasını engeller, ancak referanslanan değeri değiştiren normal atamaya izin verir.

Ancak bu tasarım vakumda var olan bir şey değildir; zaten etkin bir şekilde ref alanlarına sahip türler için kurallar tasarlamaktadır. En göze çarpan Span<T>, readonly'in burada derin olmamasına zaten güçlü bir bağımlılık gösteriyor. Birincil senaryosu, ref örneği aracılığıyla readonly alanına atama yapabilmektir.

readonly ref struct SpanOfOne
{
    readonly ref int Field;

    public ref int this[int index]
    {
        get
        {
            if (index != 1)
                throw new Exception();
            return ref Field;
        }
    }
}

Bu, readonlyyüzeysel yorumunu seçmemiz gerektiği anlamına gelir.

Modelleme oluşturucuları

İnce tasarım sorularından biri şudur: Yapıcı gövdeleri ref güvenliği için nasıl modellenir? Temel olarak aşağıdaki oluşturucu nasıl analiz edilir?

ref struct S
{
    ref int field;

    public S(ref int f)
    {
        field = ref f;
    }
}

Kabaca iki yaklaşım vardır:

  1. güvenli bağlam çağıran bağlam yerel olduğu bir yöntemi olarak model oluşturma
  2. "static yöntemi olarak, this'in bir out parametresi olduğu modeli oluşturun."

Ayrıca bir oluşturucu, aşağıdaki değişmezleri karşılamalıdır:

  1. ref parametrelerinin ref alanları olarak yakalanaadığından emin olun.
  2. ref alanlarının, this parametreleriyle ref'e kaçamayacağından emin olun. Bu, karmaşık başvuru atamasını ihlal eder.

Amaç, oluşturucular için herhangi bir özel kural kullanılmadan sabit değerlerimizi karşılayan formu seçmektir. Oluşturucular için en iyi modelin this bir out parametresi olarak görüntülemek olduğu düşünüldüğünde. yalnızca doğasını döndüren out, yukarıdaki tüm invariantları özel bir duruma ihtiyaç duymadan karşılamamızı sağlar.

public static void ctor(out S @this, ref int f)
{
    // The ref-safe-context of `ref f` is *return-only* which is also the 
    // safe-context of `this.field` hence this assignment is allowed
    @this.field = ref f;
}

Yöntem bağımsız değişkenleri eşleşmelidir

Yöntem argümanlarının kuralla uyumlu olması gerektiği kuralı, geliştiriciler için yaygın bir karışıklık kaynağıdır. Kuralın arkasındaki mantığı bilmiyorsanız anlaşılması zor olan bir dizi özel durum içeren bir kuraldır. Kuralın nedenlerini daha iyi anlamak için başvuru-güvenli bağlam ve güvenli bağlam'ü sadece bağlamolarak basitleştireceğiz.

Yöntemler, parametre olarak kendilerine geçirilen durumu oldukça liberal bir şekilde döndürebilir. Temelde, kapsam dışı olan herhangi bir ulaşılabilir durum ( refile döndürülme dahil) döndürülebilir. Bu, doğrudan bir return deyimi aracılığıyla veya ref değerine atanarak dolaylı olarak döndürülebilir.

Doğrudan iadeler, geri dönüş güvenliği için çok fazla sorun oluşturmaz. Derleyicinin yalnızca bir yönteme döndürülen tüm girişlere bakması gerekir ve ardından dönüş değerini girişin en düşük bağlam olacak şekilde etkili bir şekilde kısıtlar. Bu dönüş değeri normal işlemeden geçer.

Tüm ref metot için hem giriş hem de çıkış olduklarından dolaylı dönüşler önemli bir sorun oluşturur. Bu çıkışlarınbilinen bir bağlamı zaten vardır. Derleyici yenilerini çıkarsayamaz, bunları mevcut seviyede dikkate almak zorunda. Başka bir deyişle, derleyici çağrılan yöntemde atanabilir her bir 'ı incelemeli, onunbağlamını değerlendirmeli ve ardından yönteme döndürülebilir hiçbir girişin bu 'tan daha küçük bir bağlamına sahip olmadığını doğrulamalıdır. Böyle bir durum varsa, yöntem çağrısının ref güvenliğini ihlal edebileceğinden yasa dışı olması gerekir.

Yöntem bağımsız değişkenleri, derleyicinin bu güvenlik denetimini onaylama işlemiyle eşleşmelidir.

Bunu değerlendirmenin geliştiricilerin dikkate almaları genellikle daha kolay olan farklı bir yöntem aşağıdaki alıştırmayı yapmaktır:

  1. Yöntem tanımına bakın, durumun dolaylı olarak döndürülebileceği tüm yerleri belirleyin: a. ref b'ye işaret eden değiştirilebilir ref struct parametreleri. Atanabilir başvuru ref alanlarına sahip değiştirilebilir ref parametreleri. ref işaret eden atanabilir ref parametreleri veya ref struct alanları (özyinelemeli olarak göz önünde bulundurun)
  2. Arama sitesi A'ya bakın. B'nin üzerinde tanımlanan konumlarla hizalanan bağlamları belirleyin. Yöntemine döndürülen tüm girişlerin bağlamlarını tanımlayın (scoped parametreleriyle hizalanmayın)

2.b'deki herhangi bir değer 2.a'dan küçükse yöntem çağrısı geçersiz olmalıdır. Şimdi kuralları göstermek için birkaç örneğe bakalım:

ref struct R { }

class Program
{
    static void F0(ref R a, scoped ref R b) => throw null;

    static void F1(ref R x, scoped R y)
    {
        F0(ref x, ref y);
    }
}

F0 çağrısına baktığımızda (1) ve (2) üzerinden geçelim. Dolaylı dönüş potansiyeline sahip parametreler, her ikisi de doğrudan atanabileceği için a ve b'dir. Bu parametrelere göre sıralanan bağımsız değişkenler şunlardır:

  • a, x'i olan, çağıran-bağlamı ile 'e eşlenir.
  • b, y'nin bağlamı ile sahip olduğu ile eşlendiği

yöntemine döndürülebilir giriş kümesi şunlardır:

  • x ile kaçış kapsamıçağıran bağlamı
  • ref x ile kaçış kapsamıçağıran bağlamı
  • y, kaçış kapsamı olan işlev üyesi

ref y değeri, bir scoped ref'e eşlendiği için döndürülemez ve bu nedenle bir giriş olarak kabul edilmez. Ancak, en az bir girdinin ( bağımsız değişkeni) çıkışlardan birinin (y bağımsız değişkeni) sahip olduğundan daha küçük bir x olduğu göz önüne alındığında, yöntem çağrısı geçerli değildir.

Farklı bir varyasyon aşağıdaki gibidir:

ref struct R { }

class Program
{
    static void F0(ref R a, ref int b) => throw null;

    static void F1(ref R x)
    {
        int y = 42;
        F0(ref x, ref y);
    }
}

Tekrar dolaylı dönüş potansiyeli olan parametreler, her ikisi de doğrudan atanabileceği için a ve b'dir. Ancak b, bir ref struct işaret etmediğinden dışlanabilir, bu nedenle ref durumunu depolamak için kullanılamaz. Bu nedenle:

  • a, x'i olan, çağıran-bağlamı ile 'e eşlenir.

yöntemine döndürülebilir giriş kümesi şunlardır:

  • x ile bağlamçağıran bağlamı
  • ref x ile bağlamçağıran bağlamı
  • ref y ile bağlamişlev üyesi

En az bir girişin ( bağımsız değişkeni), çıkışlardan birinin (ref y bağımsız değişkeni) daha küçük bir x'ye sahip olması durumunda, yöntem çağrısı yasadışıdır.

Yöntem bağımsız değişkenlerinin kuralla eşleşmesi gerektiği mantık işte budur. Hem girişlerin dikkate alınmaktan çıkarılmasının bir yolu olarak scoped, hem de readonly’nin çıkış olarak kaldırılmasının bir yolu olarak ref düşünülerek daha da ileri gider (readonly ref'e atanamaz, bu yüzden çıkış kaynağı olamaz). Bu özel durumlar kurallara karmaşıklık katar, ancak bunu geliştiricinin yararına yapar. Derleyici, bir üyeyi çağırırken geliştiricilere maksimum esneklik sağlamak için sonuca katkıda bulunamayacaını bildiği tüm girişleri ve çıkışları kaldırmaya çalışır. Aşırı yük çözümlemesi gibi, tüketicilere daha fazla esneklik sağladığında kurallarımızı daha karmaşık hale getirmek için çaba harcamaya değer.

Bildirim ifadelerinin çıkarımlı güvenli bağlam örnekleri

bildirim ifadelerinin güvenli bağlamı ile çıkarım yapma ile ilgilidir.

ref struct RS
{
    public RS(ref int x) { } // assumed to be able to capture 'x'

    static void M0(RS input, out RS output) => output = input;

    static void M1()
    {
        var i = 0;
        var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M2(RS rs1)
    {
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M3(RS rs1)
    {
        M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
    }
}

scoped değiştiricisinden elde edilen yerel bağlamın değişken için kullanılabilecek en dar bağlam olduğunu unutmayın; daha dar olması, ifadenin yalnızca ifadeden daha dar bir bağlamda bildirilen değişkenlere başvurduğu anlamına gelir.