Meraklı null birleştirme operatörü özel örtük dönüşüm davranışı


542

Not: Bu, Roslyn'de düzeltilmiş gibi görünüyor

Cevabımı yazarken bu soru ortaya çıktı bu bir bir çağrışımsal bahsediyor boş-kaynaştırma operatörü .

Hatırlatma olarak, boş birleştirici operatörün fikri, formun bir ifadesi

x ?? y

önce değerlendirir x, sonra:

  • Değeri xnull ise y, değerlendirilir ve bu ifadenin sonucudur
  • Değeri ise xboş olmayan, ybir değil, değerlendirilir ve değeri xderleme zamanı türüne dönüşüm sonra, ifade sonucudur ygerektiğinde

Şimdi genellikle bir dönüşüme gerek yoktur, ya da sadece boş değer türünden boş değer olmayan bir biçime - genellikle türler aynıdır ya da yalnızca (diyelim) int?ile int. Ancak, can kendi örtük dönüştürme operatörleri oluşturmak ve bu gerektiğinde kullanılır.

Basit bir durum için x ?? y, garip bir davranış görmedim. Ancak,(x ?? y) ?? z bazı kafa karıştırıcı davranışlar görüyorum.

İşte kısa ama eksiksiz bir test programı - sonuçlar yorumlarda:

using System;

public struct A
{
    public static implicit operator B(A input)
    {
        Console.WriteLine("A to B");
        return new B();
    }

    public static implicit operator C(A input)
    {
        Console.WriteLine("A to C");
        return new C();
    }
}

public struct B
{
    public static implicit operator C(B input)
    {
        Console.WriteLine("B to C");
        return new C();
    }
}

public struct C {}

class Test
{
    static void Main()
    {
        A? x = new A();
        B? y = new B();
        C? z = new C();
        C zNotNull = new C();

        Console.WriteLine("First case");
        // This prints
        // A to B
        // A to B
        // B to C
        C? first = (x ?? y) ?? z;

        Console.WriteLine("Second case");
        // This prints
        // A to B
        // B to C
        var tmp = x ?? y;
        C? second = tmp ?? z;

        Console.WriteLine("Third case");
        // This prints
        // A to B
        // B to C
        C? third = (x ?? y) ?? zNotNull;
    }
}

Bu yüzden üç özel değer türümüz var A, BveC C B, C, A ve B, A ile ilgili dönüşüm ile,

Hem ikinci hem de üçüncü vakayı anlayabiliyorum ... ama neden ilk durumda ekstradan A'ya B dönüşümü var? Özellikle, gerçekten aynı şey olması ilk vaka ve ikinci durumda umuyordum - bu sadece tüm sonra, yerel bir değişkene bir ifade ayıklanması ediyor.

Neler olup bittiğini merak eden var mı? Ben C # derleyicisi söz konusu olduğunda "hata" ağlamak için son derece hesistant, ama ne olup bittiği gibi güdük ...

DÜZENLEME: Tamam, yapılandırıcının yanıtı, bunun bir hata olduğunu düşünmem için daha fazla neden veren, neler olup bittiğine dair nazik bir örnek. EDIT: Örnek şimdi iki boş birleştirici operatöre bile gerek yok ...

using System;

public struct A
{
    public static implicit operator int(A input)
    {
        Console.WriteLine("A to int");
        return 10;
    }
}

class Test
{
    static A? Foo()
    {
        Console.WriteLine("Foo() called");
        return new A();
    }

    static void Main()
    {
        int? y = 10;

        int? result = Foo() ?? y;
    }
}

Bunun çıktısı:

Foo() called
Foo() called
A to int

Foo()Burada iki kez çağrılan gerçek benim için çok şaşırtıcı - ifadenin iki kez değerlendirilmesi için herhangi bir neden göremiyorum .


32
Onlar :) "kimsenin bu şekilde kullanacağız" düşünce bahis
cyberzed

57
Daha kötü bir şey görmek ister misiniz? Tüm örtülü dönüşüm ile bu hattı kullanmayı deneyin: C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);. Şunları alacaksınız:Internal Compiler Error: likely culprit is 'CODEGEN'
yapılandırıcı

5
Ayrıca, aynı kodu derlemek için Linq ifadelerini kullanırken bunun gerçekleşmeyeceğini unutmayın.
yapılandırıcı

8
@Peter beklenmedik desen, ancak akla yatkın(("working value" ?? "user default") ?? "system default")
Factor Mystic

23
@ yes123: Sadece dönüşümle uğraşırken, tamamen ikna olmadım. Bir yöntemi iki kez yürüttüğünü görmek, bunun bir hata olduğunu oldukça açık hale getirdi. Yanlış görünen ama aslında tamamen doğru olan bazı davranışlara hayran kalacaksınız . C # takımı benden daha akıllı - bir şeyin onların hatası olduğunu kanıtlayana kadar aptal olduğumu varsayıyorum.
Jon Skeet

Yanıtlar:


418

Bu konunun analizine katkıda bulunan herkese teşekkürler. Açıkça bir derleyici hatası. Bu, yalnızca birleştirme operatörünün sol tarafında iki boş değer türünü içeren kaldırılmış bir dönüşüm olduğunda gerçekleşir.

Henüz tam olarak nerede yanlış gittiğini tespit etmedim, ama derleme "nulllable düşürme" aşamasında bir noktada - ilk analizden sonra ama kod üretmeden önce - biz ifade azaltmak

result = Foo() ?? y;

yukarıdaki örnekten aşağıdakilerin ahlaki eşdeğerine:

A? temp = Foo();
result = temp.HasValue ? 
    new int?(A.op_implicit(Foo().Value)) : 
    y;

Açıkçası bu yanlış; doğru indirme

result = temp.HasValue ? 
    new int?(A.op_implicit(temp.Value)) : 
    y;

Şimdiye kadar yaptığım analize dayanarak en iyi tahminim, nullable optimizer'ın buradaki raylardan çıkması. Sıfırlanabilir türdeki belirli bir ifadenin büyük olasılıkla boş olamayacağını bildiğimiz durumları arayan, boş bir optimize edicimiz var. Aşağıdaki naif analizi düşünün: ilk önce şunu söyleyebiliriz:

result = Foo() ?? y;

aynıdır

A? temp = Foo();
result = temp.HasValue ? 
    (int?) temp : 
    y;

ve sonra şunu söyleyebiliriz

conversionResult = (int?) temp 

aynıdır

A? temp2 = temp;
conversionResult = temp2.HasValue ? 
    new int?(op_Implicit(temp2.Value)) : 
    (int?) null

Ancak optimize edici adım atabilir ve "whoa, bir dakika bekleyin, temp'in null olmadığını zaten kontrol ettik; sadece yükseltilmiş bir dönüşüm operatörü çağırdığımız için ikinci kez null için kontrol etmeye gerek yok". Onları sadece

new int?(op_Implicit(temp2.Value)) 

Benim tahminim, bir yerde optimize edilmiş formunun olduğu gerçeğini önbelleğe aldığımızdır (int?)Foo(), new int?(op_implicit(Foo().Value))ancak aslında istediğimiz optimize edilmiş form değildir; Foo () 'nun optimize edilmiş formunu istiyoruz - bunun yerine geçici ve sonra dönüştürülmüş.

C # derleyicisindeki birçok hata, kötü önbellekleme kararlarının bir sonucudur. Bilge bir kelime: bir gerçeği daha sonra kullanmak üzere her önbelleğe aldığınızda, ilgili bir değişiklik olması durumunda potansiyel olarak bir tutarsızlık yaratabilirsiniz . Bu durumda, ilk analizden sonra değişen şey, Foo () çağrısının her zaman geçici bir getirme olarak gerçekleştirilmesidir.

C # 3.0'da sıfırlanabilir yeniden yazma geçişinin çok fazla yeniden düzenlenmesi yaptık. Hata, C # 3.0 ve 4.0'da çoğalır ancak C # 2.0'da çoğalmaz, bu da hatanın muhtemelen benim kötü olduğum anlamına gelir. Afedersiniz!

Veritabanına bir hata gireceğim ve bunu dilin gelecekteki bir sürümü için düzeltip düzeltemeyeceğimizi göreceğiz. Analiziniz için herkese tekrar teşekkürler; Çok yardımcı oldu!

GÜNCELLEME: Sıfırlanabilir optimize ediciyi Roslyn için sıfırdan yeniden yazdım; şimdi daha iyi bir iş çıkarıyor ve bu tür garip hatalardan kaçınıyor. Roslyn'deki optimize edicinin nasıl çalıştığına dair bazı düşünceler için buradan başlayan makaleler dizisine bakın: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/


1
@Eric Bunun da açıklayacağını merak ediyorum: connect.microsoft.com/VisualStudio/feedback/details/642227
MarkPflug

12
Artık Roslyn'in Son Kullanıcı Önizlemesi'ne sahip olduğuma göre, bunun sabit olduğunu onaylayabilirim. (Yine de yerli C # 5 derleyicisinde mevcut.)
Jon Skeet

84

Bu kesinlikle bir hatadır.

public class Program {
    static A? X() {
        Console.WriteLine("X()");
        return new A();
    }
    static B? Y() {
        Console.WriteLine("Y()");
        return new B();
    }
    static C? Z() {
        Console.WriteLine("Z()");
        return new C();
    }

    public static void Main() {
        C? test = (X() ?? Y()) ?? Z();
    }
}

Bu kodun çıktısı:

X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)

Bu bana her ??birleşme ifadesinin ilk bölümünün iki kez değerlendirildiğini düşündürdü . Bu kod bunu kanıtladı:

B? test= (X() ?? Y());

çıktılar:

X()
X()
A to B (0)

Bu, yalnızca ifade iki boş değer türü arasında bir dönüştürme gerektirdiğinde gerçekleşir; Taraflardan biri bir dize olan çeşitli permütasyonlar denedim ve hiçbiri bu davranışa neden oldu.


11
Vay - ifadeyi iki kez değerlendirmek gerçekten çok yanlış görünüyor. İyi gördüm.
Jon Skeet

Kaynakta yalnızca bir yöntem çağrınız olup olmadığını görmek biraz daha kolaydır - ancak yine de bunu çok net bir şekilde gösterir.
Jon Skeet

2
Soruma "çifte değerlendirme" nin biraz daha basit bir örneğini ekledim.
Jon Skeet

8
Tüm yöntemlerinizin "X ()" çıktısı alması gerekiyor mu? Konsola hangi yöntemin gerçekten çıktı verdiğini söylemeyi biraz zorlaştırır.
jeffora

2
X() ?? Y()Dahili olarak genişliyor gibi görünüyor X() != null ? X() : Y(), bu yüzden neden iki kez değerlendirilecek.
Cole Johnson

54

Sol gruplandırılmış vaka için oluşturulan koda bir bakarsanız, aslında böyle bir şey yapar ( csc /optimize-):

C? first;
A? atemp = a;
B? btemp = (atemp.HasValue ? new B?(a.Value) : b);
if (btemp.HasValue)
{
    first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value);
}

Başka bulmak, eğer kullanmak first hem eğer bir kısayol oluşturur ave bgeçersiz ve dönüş vardır c. Ancak, eğer ayoksa bbu boş olmayan yeniden değerlendirir olan aörtülü dönüşüm parçası olarak Bhangisinin dönmeden önce aya da bboş olmayan.

C # 4.0 Spesifikasyonundan §6.1.4:

  • Null dönüşüm geliyorsa S?için T?:
    • Kaynak değer null( HasValueözellik false) ise, sonuç nulltürün değeridir T?.
    • Aksi takdirde, dönüştürme bir unwrapping olarak değerlendirilmektedir S?için Saltta yatan dönüştürme işlemi takip eder, Siçin Tbir ambalaj (§4.1.10) takip Tetmek T?.

Bu, ikinci ambalaj-sarma kombinasyonunu açıklamaktadır.


C # 2008 ve 2010 derleyicisi çok benzer bir kod üretir, ancak bu, C # 2005 derleyicisinden (8.00.50727.4927), yukarıdaki kod için aşağıdaki kodu üreten bir gerileme gibi görünür:

A? a = x;
B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y;
C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z;

Bunun tür çıkarsama sistemine verilen ek sihirden kaynaklanıp kaynaklanmadığını merak ediyorum.


+1, ancak dönüşümün neden iki kez gerçekleştirildiğini gerçekten açıkladığını sanmıyorum. İfadeyi yalnızca bir kez değerlendirmeli, IMO.
Jon Skeet

@Jon: Etrafta oynuyorum ve (@configurator'ın yaptığı gibi) bir İfade Ağacı'nda yapıldığında beklendiği gibi çalıştığını gördüm. Yazıma eklemek için ifadeleri temizleme üzerinde çalışıyor. O zaman bu bir "hata" olduğunu poz vermek gerekir.
user7116

@Jon: Tamam İfade Ağaçları kullanılırken (x ?? y) ?? z, çift değerlendirme olmadan sırayla değerlendirme sağlayan iç içe lambdalara dönüşür . Bu açıkça C # 4.0 derleyicisi tarafından alınan bir yaklaşım değildir. Söyleyebileceğim kadarıyla, bu özel kod yolunda bölüm 6.1.4'e çok katı bir şekilde yaklaşılır ve geçici değerlendirmeler çift değerlendirme ile sonuçlanmaz.
user7116

16

Aslında, şimdi daha açık bir örnekle, buna bir hata diyeceğim. Bu hala geçerli, ancak çifte değerlendirme kesinlikle iyi değil.

Sanki A ?? Buygulanmış gibi görünüyor A.HasValue ? A : B. Bu durumda, çok fazla döküm vardır (üçlü ?:operatör için normal dökümden sonra ). Ancak tüm bunları görmezden gelirseniz, bu nasıl uygulandığına bağlı olarak mantıklıdır:

  1. A ?? B genişler A.HasValue ? A : B
  2. Abizim x ?? y. Şuraya genişlet:x.HasValue : x ? y
  3. tüm A oluşumlarını değiştir -> (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

Burada x.HasValue, iki kez kontrol edildiğini ve x ?? ydöküm gerekiyorsa, xiki kez yayınlanacağını görebilirsiniz.

??Derleyici bir hatadan ziyade , nasıl uygulandığına dair bir eser olarak koydum . Take-Away: Yan etkileri olan örtük döküm operatörleri yaratmayın.

Nasıl ??uygulandığı etrafında dönen bir derleyici hata gibi görünüyor . Take-away: yan etkileri olan birleşik ifadeleri iç içe geçirmeyin.


Oh kesinlikle normalde böyle kod kullanmak istemem, ama bence olabilir hala "bir kez ama sadece A değerlendirilmesi ve B" bir derleyici ilk genişleme içermelidir ki hata olarak sınıflandırılmalıdır. (Yöntem çağrıları olup olmadığını düşünün.)
Jon Skeet

@Jon da olabileceğine katılıyorum - ama net olarak adlandırmam. Aslında, A() ? A() : B()bunun muhtemelen A()iki kez değerlendirileceğini görüyorum , ama A() ?? B()çok fazla değil. Ve sadece dökümde olduğu için ... Hmm .. Kendimi kesinlikle doğru davranmadığını düşünmekle konuştum.
Philip Rieck

10

Soru geçmişimden de görebileceğiniz gibi bir C # uzmanı değilim, ama bunu denedim ve bunun bir hata olduğunu düşünüyorum .... ama bir acemi olarak, her şeyi anlayamadığımı söylemeliyim bu yüzden yolum kapalı ise cevabımı silerim.

Bu bugsonuca, programınızın aynı senaryoyu ele alan ancak çok daha az karmaşık olan farklı bir sürümünü yaparak ulaştım.

Destek depoları ile üç null tamsayı özelliği kullanıyorum. Her birini 4'e ayarladım ve sonraint? something2 = (A ?? B) ?? C;

( Burada tam kod )

Bu sadece A'yı okuyor ve başka bir şey yok.

Bu ifade bana öyle geliyor ki:

  1. Parantez içinde başlayın, A'ya bakın, A'ya dönün ve A boş değilse bitirin.
  2. A null ise, B'yi değerlendirin, B null değilse bitirin
  3. A ve B boşsa, C'yi değerlendirin.

Yani, A boş olmadığı için, sadece A'ya bakar ve biter.

Örneğinizde, İlk Vaka'ya bir kesme noktası koymak, x, y ve z'nin hepsinin boş olmadığını gösterir ve bu nedenle, onlara daha az karmaşık örneğim gibi davranılmasını beklerim ... ama çok fazla olduğumdan korkuyorum C # newbie ve bu sorunun anlamını tamamen kaçırdım!


5
Jon'un örneği, null olabilecek bir yapı (an gibi yerleşik türlere "benzer" olan bir değer türü) kullanması nedeniyle biraz belirsiz bir köşe vakasıdır int. Birden fazla örtük tür dönüşüm sağlayarak davayı daha da belirsiz bir köşeye iter. Bu, derleyicinin kontrol ederken verilerin türünü değiştirmesini gerektirir null. Bu örtük tip dönüşümler nedeniyle, örneği sizinkinden farklıdır.
user7116
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.