Aracılığıyla paylaş


Otomatik varsayılan yapılar

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ındaaktarılır.

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

Şampiyon sayısı: https://github.com/dotnet/csharplang/issues/5737

Özet

Bu özellik, yapı oluşturucularında kullanıcı tarafından döndürülmeden veya kullanılmadan önce açıkça atanmamış alanları tanımlamamızı ve bunları kesin atama hataları vermek yerine örtük olarak default ile başlatmamızı sağlar.

Motivasyon

Bu teklif, dotnet/csharplang#5552 ve dotnet/csharplang#5635'te bulunan kullanılabilirlik sorunlarının yanı sıra #5563(tüm alanlar kesinlikle atanmalıdır, ancak field oluşturucu içinde erişilebilir değildir) için olası bir azaltma olarak ortaya çıkarılır.


C# 1.0'dan bu yana yapı oluşturucularının this bir parametreymiş gibi kesinlikle out ataması gerekiyordu.

public struct S
{
    public int x, y;
    public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
    {
    }
}

Ayarlayıcılar yarı otomatik özellikler üzerinde elle tanımlandığında, derleyici özelliğin atamasını yedekleme alanının atamasıyla eşdeğer olarak değerlendiremediği için bu durum sorunlara yol açar.

public struct S
{
    public int X { get => field; set => field = value; }
    public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
    {
    }
}

Ayarlayıcının ref this almadığı ancak parametre olarak out field aldığı bir şema gibi ayarlayıcılar için daha ayrıntılı kısıtlamalar getirilmesinin bazı kullanım örnekleri için çok niş ve eksik olacağını varsayıyoruz.

Üzerinde zorlandığımız temel sorunlardan biri, yapı özelliklerine manuel ayarlayıcılar uygulandığında, kullanıcıların genellikle atamaları veya mantıklarını tekrar etmek zorunda kalmasıdır.

struct S
{
    private int _x;
    public int X
    {
        get => _x;
        set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
    }

    // Solution 1: assign some value in the constructor before "really" assigning through the property setter.
    public S(int x)
    {
        _x = default;
        X = x;
    }

    // Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
    public S(int x)
    {
        _x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
    }
}

Önceki tartışma

Küçük bir grup bu sorunu inceledi ve birkaç olası çözümü değerlendirdi:

  1. Yarı otomatik özelliklerin ayarlayıcılarını elle uyguladığı durumlarda, kullanıcıların this = default atamasını yapmasını gerektir. Alan başlatıcılarda ayarlanan değerleri ortadan kaldırdığı için bunun yanlış çözüm olduğu konusunda hemfikiriz.
  2. Otomatik/yarı otomatik özelliklerin tüm yedekleme alanlarını örtük olarak başlatın.
    • Bu, "yarı otomatik özellik ayarlayıcıları" sorununu çözer ve açıkça bildirilen alanları farklı kurallar altına yerleştirir: "Alanlarımı örtük olarak başlatma, ancak otomatik özelliklerimi örtük olarak başlat."
  3. Yarı otomatik bir özelliğin yedekleme alanını atamak için bir yol sağlayın ve kullanıcıların bunu atamasını sağlayın.
    • Bu, (2) ile karşılaştırıldığında hantal olabilir. Otomatik özelliğin "otomatik" olması ve belki de alanın "otomatik" başlatılmasını içermesi gerekir. Özellik, temel alan, özelliğe bir atama yoluyla atandığında mı yoksa özellik ayarlayıcısı çağrıldığında mı karışıklığa neden olabilir.

Ayrıca, örneğin her şeyi açıkça atamak zorunda kalmadan yapılara birkaç alan başlatıcı eklemek isteyen kullanıcılardan geri bildirim aldık. Bu sorunu ve "manuel ayarlayıcı ile yarı otomatik özellik" sorununu aynı anda çözebiliriz.

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
    public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
    {
    }
}

Kesin atamayı ayarlama

üzerinde atanmamış alanlar için hatalar vermek üzere kesin bir atama analizi gerçekleştirmek yerine,örtük olarak başlatılması gereken alanları belirlemek için yaparız. Bu tür başlatma, oluşturucubaşına eklenir.

struct S
{
    int x, y;

    // Example 1
    public S()
    {
        // ok. Compiler inserts an assignment of `this = default`.
    }

    // Example 2
    public S()
    {
        // ok. Compiler inserts an assignment of `y = default`.
        x = 1;
    }

    // Example 3
    public S()
    {
        // valid since C# 1.0. Compiler inserts no implicit assignments.
        x = 1;
        y = 2;
    }

    // Example 4
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `this = default`.
        if (b)
            x = 1;
        else
            y = 2;
    }

    // Example 5
    void M() { }
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `y = default`.
        x = 1;
        if (b)
            M();

        y = 2;
    }
}

Örneklerde (4) ve (5), sonuçta elde edilen kodgen bazen alanların "çift atamalarına" sahiptir. Bu genelde uygundur, ancak bu tür çift atamalarla ilgilenen kullanıcılar için, önceden kesin atama hatası tanılaması olarak verilenleri, varsayılan olarak devre dışı olan uyarı tanılaması şeklinde yayabiliriz.

struct S
{
    int x;
    public S() // warning: 'S.x' is implicitly initialized to 'default'.
    {
    }
}

Bu tanılamanın önem derecesini "hata" olarak ayarlayan kullanıcılar C# 11 öncesi davranışı kabul eder. Bu tür kullanıcılar, manuel olarak uygulanan ayarlayıcılar nedeniyle yarı otomatik özelliklerden "mahrum bırakı"lmış durumdadır.

struct S
{
    public int X
    {
        get => field;
        set => field = field < value ? value : field;
    }

    public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
    {
        X = 1;
    }
}

İlk bakışta bu, özelliğin bir "eksiklik" gibi görünebilir, ancak aslında yapılması gereken doğru şeydir. Kullanıcı, tanılamayı etkinleştirerek derleyicinin oluşturucudaki alanlarını örtük olarak başlatmasını istemediğini söyler. Burada örtük başlatmayı önlemenin bir yolu yoktur, bu nedenle onlar için çözüm, alanı el ile bildirmek ve atamak veya alan başlatıcısı eklemek gibi el ile uygulanan bir ayarlayıcıdan farklı bir başlatma yöntemi kullanmaktır.

JIT şu anda refler aracılığıyla ölü depoları ortadan kaldırmaz, bu da bu örtük başlatmaların gerçek bir maliyeti olduğu anlamına gelir. Ama bu düzeltilebilir. https://github.com/dotnet/runtime/issues/13727

Bireysel alanları tüm örnek yerine başlatmanın aslında sadece bir optimizasyon olduğunu unutmamak gerekir. Derleyici, tüm dönüş noktalarında veya this ile ilgili herhangi bir alan dışı üye erişiminden önce kesinlikle atanmamış olan alanların değişmezini karşıladığı sürece, arzuladığı herhangi bir sezgisel yöntemi uygulamakta özgür olmalıdır.

Örneğin, bir yapıda 100 alan varsa ve bunlardan yalnızca biri açıkça başlatıldıysa, diğer 99 alan için örtülü olarak initobj oluşturmak yerine, yapının tamamında bir initobj uygulamak daha mantıklı olabilir. Ancak, diğer 99 alan için örtük olarak initobj yayan bir uygulama yine de geçerli olacaktır.

Dil belirtimine yapılan değişiklikler

Standardın aşağıdaki bölümünü ayarlıyoruz:

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access

Oluşturucu bildiriminde oluşturucu başlatıcısı yoksa, this değişkeni yapı türünün out parametresiyle tam olarak aynı şekilde davranır. Özellikle bu, değişkenin örnek oluşturucusunun her yürütme yolunda kesinlikle atanacağı anlamına gelir.

Bu dili okuyacak şekilde ayarlıyoruz:

Eğer oluşturucu bildiriminde bir oluşturucu başlatıcısı yoksa, this değişkeni, kesin atama gereksinimlerinin (out) karşılanmadığında hata olarak değerlendirilmemesi dışında, yapı türünün parametresine benzer şekilde davranır. Bunun yerine aşağıdaki davranışları tanıtacağız:

  1. this değişkeninin kendisi gereksinimleri karşılamadığında, gereksinimlerin ihlal edildiği tüm noktalarda this içindeki atanmamış tüm örnek değişkenleri, oluşturucudaki diğer kodlar çalıştırılmadan önce başlatma aşamasındaki varsayılan değere (§9.3) örtük olarak başlatılır.
  2. içinde this örnek değişkeni gereksinimleri karşılamazsa veya v içinde, herhangi bir seviyedeki iç içe geçmiş başka bir örnek değişkeni gereksinimleri karşılamazsa, başlatma aşamasında, oluşturucudaki diğer herhangi bir kod çalıştırılmadan önce v örtük olarak varsayılan değerle başlatılır.

Tasarım toplantıları

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md#definite-assignment-in-structs