C # 'da, bir try bloğunda değişkenler neden kapsamda sınırlı olarak bildiriliyor?


23

Aşağıdakilere hata işleme eklemek istiyorum:

var firstVariable = 1;
var secondVariable = firstVariable;

Aşağıdaki derlenmeyecek:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Bir try catch bloğunun diğer kod blokları gibi değişkenlerin kapsamını etkilemesi neden gereklidir? Tutarlılık-kenara bir yana, kodumuzu yeniden düzenlemeye gerek kalmadan hata işlemesiyle kodlayabilmemiz mantıklı olmaz mı?


14
A try.. catch, belirli bir kod bloğu türüdür ve tüm kod blokları gittiği sürece, bir değişkeni tanımlayamaz ve aynı değişkeni başka bir alanda kapsam olarak kullanamazsınız.
Neil

msgstr "belirli bir kod bloğu türüdür". Hangi yolla belirli? Thanks
JᴀʏMᴇᴇ

7
Kıvrımlı parantez arasındaki herhangi bir şey bir kod bloğudur. Kavram aynı olsa da, bir if ifadesini izleyerek ve for ifadesini izleyerek bunu görürsünüz. İçerikler, ana kapsamına göre daha yüksek bir kapsamdadır. Kıvrımlı parantezleri {}denemeden kullandıysanız, bunun problemli olacağından eminim .
Neil

İpucu: (IDisposable) {} ve sadece {} kullanmanın da aynı şekilde geçerli olduğunu unutmayın. Bir IDisposable ile bir kullanım kullandığınızda, başarı veya başarısızlık ne olursa olsun kaynakları otomatik olarak temizler. Bunun birkaç istisnası vardır, tıpkı tek kullanımlık olmasını beklediğiniz tüm sınıflar gibi değil ...
Julia McGuigan

1
StackOverflow'ta bu aynı soru üzerine tartışmalar var, burada: stackoverflow.com/questions/94977/…
Jon Schneider

Yanıtlar:


90

Ya kodunuz:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Şimdi firstVariable, yöntem çağrınız atılırsa, bildirilmemiş bir değişkeni ( ) kullanmaya çalışıyorsunuzdur .

Not : Yukarıdaki örnekte, "tutarlılığı bir kenara" ifade eden özgün soruya özel olarak cevap verilmektedir. Bu, tutarlılıktan başka nedenlerin olduğunu göstermektedir. Ancak Peter'ın cevabının gösterdiği gibi, kararda çok önemli bir faktör olabilecek tutarlılıktan da güçlü bir argüman var.


Ah, bu tam olarak benim peşimde olan şeydi. İmkansız olanı imkansız kılan bazı dil özellikleri olduğunu biliyordum, ancak hiçbir senaryo bulamadım. Çok teşekkürler.
JᴀʏMᴇᴇ

1
"Şimdi, yöntem çağrınız atılırsa bildirilmemiş bir değişkeni kullanmaya çalışıyorsunuzdur." Dahası, değişkeni bildirilmiş, ancak başlatılmamış gibi, atanabilecek koddan önce belirtilmiş gibi ele alarak önlendiğini varsayalım. O zaman bildirilmeyecekti, ancak yine de potansiyel olarak atanmamış olacaktı ve kesin atama analizi değerinin okunmasını yasaklayabilecekti (bunun gerçekleşebileceğini bir araya getirme ataması olmadan).
Eliah Kagan

3
C # gibi statik bir dil için, bildirim gerçekten yalnızca derleme zamanında geçerlidir. Derleyici bildirimi kapsamda daha önce kolaylıkla hareket ettirebilirdi. Çalışma zamanında en önemli gerçek, değişkenin başlatılmamış olmasıdır .
jpmc26

3
Bu cevaba katılmıyorum. C #, başlatılmamış değişkenlerin bazı veri akışı farkındalığı ile okunamayacağı kuralına zaten sahip. (Durumlarda değişkenleri bildirmeyi switchve başkalarına erişmeyi deneyin .) Bu kural burada kolayca uygulanabilir ve bu kodun yine de derlenmesini önleyebilir. Peter'ın aşağıdaki cevabının daha makul olduğunu düşünüyorum.
Sebastian Redl

2
Bildirilmemiş bir başlatılmamış ile C # bunları ayrı izler arasında bir fark vardır. Bloğun dışında, bildirildiği yerde bir değişken kullanmanıza izin verilirse, ilk catchbloğa kendinize atayabileceğiniz ve o zaman kesinlikle ikinci trybloğa atanabileceği anlamına gelir .
svick,

64

Bunun Ben tarafından iyi cevaplandığını biliyorum, ancak uygun bir şekilde kenara itilen tutarlılık POV'unu ele almak istedim. try/catchBlokların kapsamı etkilemediğini varsayarsak, sonunda:

{
    // new scope here
}

try
{
   // Not new scope
}

Ve bana bu işe üzerine kafa çöküyor az şaşkınlık (POLA) Prensibi artık çünkü {ve }kendilerinden önce ne bağlamına göre iki görevi birden yerine.

Bu karmaşadan kurtulmanın tek yolu, try/catchblokları tanımlamak için başka bir işaretleyici belirlemektir . Hangi bir kod kokusu eklemeye başlar. Bu yüzden try/catch, dilde kapsamsız bir zamana sahip olsaydınız , kapsamlı versiyonda daha iyi olacağınız bir karmaşa olurdu.


Başka bir mükemmel cevap. Ve POLA'yı hiç duymamıştım, okuma için çok güzel. Çok teşekkür ederim dostum.
JᴀʏMᴇᴇ

“Bu karmaşadan kurtulmanın tek yolu, tasfiye try/ catchbloklara ayırmak için başka bir belirteç belirlemek .” - Nasıl yani: try { { // scope } }? :)
CompuChip

@ ComppuChip, {}çifte görevi kapsam olarak alan ve kapsamına bağlı olarak kapsam yaratmayan kapsam niteliğindedir . try^ //no-scope ^farklı bir marker örneği olacaktır.
Leliel

1
Benim düşünceme göre, bu çok daha temel sebep ve "gerçek" cevabına daha yakın.
Jack Aidley

@JackAidley, özellikle zaten atanmamış bir değişkeni kullandığınız yere kod yazabildiğiniz için. Bu yüzden Ben'in cevabı, bunun yararlı bir davranış olduğuna dair bir noktaya sahip olsa da, davranışın neden var olduğunu anlamıyorum. Ben'in cevabı OP'nin “tutarlılık uğruna bir kenara” dediğini söylüyor, ancak tutarlılık mükemmel bir sebep! Dar kapsamın başka türlü yararları vardır.
Kat

21

Tutarlılık-kenara bir yana, kodumuzu yeniden düzenlemeye gerek kalmadan hata işlemesiyle kodlayabilmemiz mantıklı olmaz mı?

Buna cevap vermek için, bir değişkenin kapsamından daha fazlasına bakmak gerekir .

Değişken kapsamda kalsa bile, kesinlikle atanmaz .

Değişkeni try bloğunda bildirmek - derleyiciye ve insan okuyuculara - yalnızca bu bloğun içinde anlamlı olduğunu ifade eder. Derleyicinin bunu zorlaması yararlıdır.

Değişkenin try bloğundan sonra kapsamda olmasını istiyorsanız, bunu bloğun dışında ilan edebilirsiniz:

var zerothVariable = 1_000_000_000_000L;
int firstVariable;

try {
    // Change checked to unchecked to allow the overflow without throwing.
    firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
    Console.Error.WriteLine(e.Message);
    Environment.Exit(1);
}

Bu, değişkenin try bloğu dışında anlamlı olabileceğini ifade eder. Derleyici buna izin verecektir.

Ancak, değişkenleri bir try bloğuna koyduktan sonra kapsamda tutmanın genellikle yararlı olmayacağının başka bir nedenini de gösterir. C # derleyicisine kesin atama analizi yapılır ve kanıtlanmamış bir değişkenin değerinin okunmasına izin verilmez. Yani hala değişkenden okuyamıyorsunuz.

Diyelim ki try bloğundan sonra değişkenden okumaya çalışıyorum:

Console.WriteLine(firstVariable);

Yani verecek bir derleme zamanı hatası :

CS0165 Atanmamış yerel değişkenin 'firstVariable' kullanımı

Aradım Environment.Exit böylece, catch bloğunda ben değişken öncesinde Console.WriteLine çağrıya atanmıştır biliyoruz. Ancak derleyici bu çıkarım yapmaz.

Derleyici neden bu kadar katı?

Bunu bile yapamam:

int n;

try {
    n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}

Console.WriteLine(n);

Bu kısıtlamaya bakmanın bir yolu, C # 'daki kesin atama analizinin çok karmaşık olmadığını söylemektir. Ancak buna bakmanın bir başka yolu, catch cümlecikleri olan bir try bloğuna kod yazdığınızda, hem derleyiciye hem de herhangi bir insan okuyucusuna, hepsinin çalışamayacağı gibi davranılması gerektiğini söylemenizdir.

Ne demek istediğimi göstermek için, derleyicinin yukarıdaki koda izin verip vermediğini düşünün, ancak daha sonra kişisel olarak bildiğiniz bir istisna atmayacak bir işleve try bloğuna bir çağrı eklediniz . Çağrılan işlevin bir atamadığını garanti edememek IOException, derleyici natandığını bilmiyordu ve sonra yeniden ateşlemeniz gerekecekti.

Diğer bir deyişle, catch cümleciklerine sahip bir try bloğuna atanmış bir değişkenin kesinlikle sonradan atanıp atanmadığının belirlenmesinde oldukça karmaşık bir analiz yapılmasından önce derleyici, daha sonra kırılması muhtemel kod yazmaktan kaçınmanıza yardımcı olur. (Sonuçta, bir istisna yakalamak, genellikle birinin atılabileceğini düşündüğünüz anlamına gelir.)

Değişkenin tüm kod yollarından atandığından emin olabilirsiniz.

Kod derlemesini değişkene try bloğundan önce veya catch bloğunda bir değer vererek derleyebilirsiniz. Bu şekilde, try bloğundaki atama gerçekleşmese bile, yine de başlatılmış veya atanmış olacaktır. Örneğin:

var n = 0; // But is this meaningful, or just covering a bug?

try {
    n = 10;
}
catch (IOException) {
}

Console.WriteLine(n);

Veya:

int n;

try {
    n = 10;
}
catch (IOException) {
    n = 0; // But is this meaningful, or just covering a bug?
}

Console.WriteLine(n);

Bu derler. Ama varsayılan değer bunu mantıklı vermek keşke böyle bir şey yapmak en iyisidir * ve doğru davranış üretir.

Değişkeni try bloğunda ve tüm catch bloklarında atadığınız bu ikinci durumda, try-catch'ten sonra değişkeni okuyabilseniz de, değişkeni ekli bir finallybloğun içinde okuyamazsınız , çünkü İnfaz, düşündüğümüzden daha fazla durumda bir deneme bloğu bırakabilir .

* Bu arada, C ve C ++ gibi bazı dillerin her ikisi de başlatılmamış değişkenlere izin verir ve bunlardan okumayı engellemek için kesin atama çözümlemelerine sahip değildir . Başlatılmamış belleğin okunması, programların nitel olmayan ve düzensiz bir şekilde davranmasına neden olduğundan , genellikle bir başlatıcı sağlamadan bu dillerde değişkenleri eklemekten kaçınılması önerilir . C # ve Java gibi kesin atama analizine sahip dillerde, derleyici, başlatılmamış değişkenleri okumaktan ve ayrıca daha sonra anlamlı olarak yanlış yorumlanabilecek anlamsız değerlerle başlatılmasından daha az kötülükten sizi kurtarır.

Değişken atanmamış kod yollarını bir istisna (veya geri dönüş) atayarak yapabilirsiniz.

Bazı eylemler gerçekleştirmeyi (günlük tutma gibi) planlıyorsanız ve istisnayı yeniden başlatırsanız veya başka bir istisna atarsanız ve bu, değişkenin atanmamış olduğu herhangi bir catch cümlesinde gerçekleşirse, derleyici değişkenin atandığını bilir:

int n;

try {
    n = 10;
}
catch (IOException e) {
    Console.Error.WriteLine(e.Message);
    throw;
}

Console.WriteLine(n);

Bu derler ve makul bir seçim olabilir. İstisna sadece atılmış olmadıkça Ancak, fiili uygulamalarda, hatta kurtarmayı denemek için bir anlam ifade etmiyor durumlar * , hala alıcı emin olmalı ve düzgün işleme yere .

(Bu durumda bir nihayet bloğundaki değişkeni de okuyamazsınız, ancak bunu yapmanız gerektiği gibi hissetmezsiniz - sonuçta, nihayetinde esasen her zaman çalışan bloklar ve bu durumda değişken her zaman atanmaz .)

* Örneğin, çoğu uygulama OutOfMemoryException'ı işleyen bir yakalama yan tümcesine sahip değildir, çünkü bu konuda yapabilecekleri herhangi bir şey en azından çarpma kadar kötü olabilir .

Belki gerçekten do kod refactor istiyor.

Örneğinizde, tanıtmak firstVariableve secondVariabledenemek blokları. Söylediğim gibi, onları atandıkları deneme bloklarından önce tanımlayabilir, böylece daha sonra kapsam dahilinde kalabilir ve derleyiciyi her zaman atandıklarından emin olarak onlardan okumanızı sağlayacak şekilde kandırabilir / kandırabilirsiniz.

Ancak bu bloklardan sonra görünen kod muhtemelen doğru şekilde atanmasına bağlıdır. Bu durumda, kodunuz bunu yansıtmalı ve sağlamalıdır.

Öncelikle, oradaki hatayı gerçekten halledebilir misiniz (ve gerekir)? İstisna işlemenin ortaya çıkmasının nedenlerinden biri, oluştukları yere yakın olmasa bile, etkili bir şekilde ele alınabilecek hataları ele almayı kolaylaştırmaktır .

Baştaki ve bu değişkenleri kullanan işlevdeki hatayı gerçekten işleyemiyorsanız, belki try bloğu o işlevde olmamalı, bunun yerine daha yüksek bir yerde (örneğin, bu işlevi çağıran kodda veya kodda) olmalıdır. çağrıları olduğunu o kodu). Yanlışlıkla başka bir yere atılan bir istisnayı yakalamadığınızdan ve yanlış bir şekilde başlatılırken firstVariableve a secondVariable.

Başka bir yaklaşım, değişkenleri kullanan kodu try bloğuna koymaktır. Bu genellikle makuldür. Yine, başlatıcılarından yakaladığınız istisnalar dışında kalan kodlardan da atılabilirse, bunları kullanırken bu olasılığı ihmal etmediğinizden emin olmalısınız.

(Örneklerinizde gösterilenden daha karmaşık ifadelerle değişkenleri başlattığınızı, aslında bir istisna atabileceklerini ve ayrıca tüm olası istisnaları yakalamak için değil , sadece özel istisnaları yakalamak için planladığınızı farz ediyorum. yapabilirsin Öngörülebilir ve anlamlı bir şekilde başa Gerçek dünyanın her zaman bu kadar iyi olmadığı ve üretim kodunun bazen bunu yaptığı doğrudur , ancak buradaki amacınız iki belirli değişkeni başlatırken meydana gelen hataları ele almak olduğu için, bu belirli için yazdığınız herhangi bir catch cümlesi amaç ne olursa olsun her türlü hataya özel olmalıdır.)

Üçüncü bir yol başarısız olabilecek kodu ve onu kullanan try-catch'i kendi yöntemine çıkarmaktır . Bu, önce tamamen hatalarla baş etmek isteyip istemediğiniz ve daha sonra başka bir yerde ele alınması gereken bir istisnayı istemeden yakalamaktan endişe duymamanız durumunda faydalıdır.

Örneğin, herhangi bir değişkeni atayamazsanız uygulamadan hemen çıkmak istediğinizi varsayalım. (Açıkçası, tüm istisnaların ele alınması ölümcül hatalar için değildir; bu sadece bir örnektir ve uygulamanızın soruna nasıl tepki vermesini istediğinizi olabilir veya olmayabilir.) Buna benzer bir şey yapabilirsiniz:

// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
    try {
        // This code is contrived. The idea here is that obtaining the values
        // could actually fail, and throw a SomeSpecificException.
        var firstVariable = 1;
        var secondVariable = firstVariable;
        return (firstVariable, secondVariable);
    }
    catch (SomeSpecificException e) {
        Console.Error.WriteLine(e.Message);
        Environment.Exit(1);
        throw new InvalidOperationException(); // unreachable
    }
}

// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
    var (firstVariable, secondVariable) = GetFirstAndSecondValues();

    // Code that does something with them...
}

Bu kod , birden fazla değer döndürmek için C # 7.0'ın sözdizimine sahip bir ValueTuple döndürür ve yapıyı kaldırır , ancak hala daha önceki bir C # sürümündeyseniz, bu tekniği kullanabilirsiniz; örneğin, parametreleri kullanabilir veya her iki değeri de sağlayan özel bir nesne döndürebilirsiniz. . Ayrıca, eğer iki değişken aslında sıkı bir şekilde ilişkili değilse, yine de iki ayrı yönteme sahip olmak muhtemelen daha iyi olacaktır.

Özellikle bunun gibi birden fazla yönteminiz varsa, kullanıcıya ölümcül hataları bildirme ve bırakma konusunda kodunuzu merkezileştirmelisiniz. (Örneğin, parametresi Dieolan bir yöntem yazabilirsiniz message.)throw new InvalidOperationException(); Çizgi aslında hiçbir zaman yürütülür değil ihtiyacınız kadar (ve olmamalıdır) bunun için bir yakalama maddesi yazın.

Belirli bir hata oluştuğunda bırakmanın dışında , orijinal istisnayı saran başka bir tür istisna atarsanız, bazen buna benzer bir kod yazabilirsiniz . (Bu durumda, sen değil ikinci, ulaşılamaz atış ifadesi gerekir.)

Sonuç: Kapsam resmin sadece bir kısmıdır.

Kodunuzu yeniden işleme koymadan hata işleme ile (veya tercih ederseniz, neredeyse hiç yeniden işleme koymadan ), yalnızca değişkenlerin bildirimlerini atadıklarından ayırarak sarma işlemini gerçekleştirebilirsiniz . Derleyici, eğer C # 'nın belirli atama kurallarını yerine getirirseniz ve try bloğunun önünde bir değişken olduğunu bildirirseniz daha geniş kapsamı açık hale gelir. Ancak daha fazla refactoring hala en iyi seçenek olabilir.


"catch cümlecikleri olan bir try bloğuna kod yazdığınızda, hem derleyiciye hem de herhangi bir insan okuyucusuna, hepsinin çalışamayabileceği gibi davranılması gerektiğini söylüyorsunuz." Derleyicinin umursadığı şey, önceki ifadeler istisna olsa bile, denetimin daha sonraki ifadelere ulaşmasıdır. Derleyici normalde bir ifade bir istisna atarsa ​​bir sonraki ifadenin çalıştırılmayacağını ve atanmamış bir değişkenin okunmayacağını varsayar. Bir 'catch' eklemek, kontrolün daha sonraki ifadelere ulaşmasına izin verecektir - bu, try bloğundaki kodun atıp atmamasının önemi yok.
Pete Kirkham,
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.