รูปแบบเพื่อหลีกเลี่ยงการลองจับบล็อกซ้อนกัน?


114

พิจารณาสถานการณ์ที่ฉันมีสามวิธี (หรือมากกว่า) ในการคำนวณซึ่งแต่ละวิธีอาจล้มเหลวโดยมีข้อยกเว้น ในการพยายามคำนวณแต่ละครั้งจนกว่าเราจะพบสิ่งที่สำเร็จฉันได้ดำเนินการดังต่อไปนี้:

double val;

try { val = calc1(); }
catch (Calc1Exception e1)
{ 
    try { val = calc2(); }
    catch (Calc2Exception e2)
    {
        try { val = calc3(); }
        catch (Calc3Exception e3)
        {
            throw new NoCalcsWorkedException();
        }
    }
}

มีรูปแบบใดบ้างที่ได้รับการยอมรับซึ่งประสบความสำเร็จในวิธีที่ดีกว่านี้? แน่นอนว่าฉันสามารถสรุปการคำนวณแต่ละรายการด้วยวิธีตัวช่วยซึ่งส่งคืนค่าว่างเมื่อเกิดความล้มเหลวจากนั้นใช้ตัว??ดำเนินการ แต่มีวิธีการทำเช่นนี้โดยทั่วไปหรือไม่ (กล่าวคือโดยไม่ต้องเขียนวิธีการช่วยเหลือสำหรับแต่ละวิธีที่ฉันต้องการใช้ )? ฉันคิดเกี่ยวกับการเขียนวิธีการคงที่โดยใช้ชื่อสามัญซึ่งรวมวิธีการใด ๆ ในการลอง / จับและส่งคืนค่าว่างเมื่อล้มเหลว แต่ฉันไม่แน่ใจว่าฉันจะทำอย่างไร ความคิดใด ๆ ?


คุณสามารถใส่รายละเอียดเกี่ยวกับการคำนวณได้หรือไม่?
James Johnson

2
โดยพื้นฐานแล้วเป็นเพียงวิธีการแก้ / ประมาณ PDE ที่แตกต่างกัน มันมาจากไลบรารีของบุคคลที่สามดังนั้นฉันจึงไม่สามารถแก้ไขให้ส่งคืนรหัสข้อผิดพลาดหรือโมฆะได้ สิ่งที่ดีที่สุดที่ฉันทำได้คือห่อทีละวิธี
jjoelson

วิธีการคำนวณเป็นส่วนหนึ่งของโครงการของคุณหรือไม่ (แทนที่จะเป็นไลบรารีของบุคคลที่สาม) ถ้าเป็นเช่นนั้นคุณสามารถดึงตรรกะที่แสดงข้อยกเว้นและใช้เพื่อตัดสินใจว่าต้องเรียกวิธีการคำนวณใด
คริส

1
มีกรณีการใช้งานอื่นสำหรับสิ่งนี้ที่ฉันเจอใน Java - ฉันต้องแยกวิเคราะห์ a StringไปยังการDateใช้งานSimpleDateFormat.parseและฉันต้องลองใช้รูปแบบต่างๆตามลำดับย้ายไปยังรายการถัดไปเมื่อมีข้อยกเว้น
ตัวแปรที่น่าสังเวช

คำตอบ:


125

เท่าที่จะทำได้อย่าใช้ข้อยกเว้นสำหรับขั้นตอนการควบคุมหรือสถานการณ์ที่ไม่เป็นที่ยอมรับ

แต่เพื่อตอบคำถามของคุณโดยตรง (สมมติว่าประเภทข้อยกเว้นเหมือนกันทั้งหมด):

Func<double>[] calcs = { calc1, calc2, calc3 };

foreach(var calc in calcs)
{
   try { return calc(); }
   catch (CalcException){  }
} 

throw new NoCalcsWorkedException();

15
นี้อนุมานว่าCalc1Exception, Calc2ExceptionและCalc3Exceptionแบ่งปันชั้นฐานร่วมกัน
Wyzard

3
ด้านบนเขาถือว่าเป็นลายเซ็นทั่วไปซึ่งไม่ไกลนัก คำตอบที่ดี
TomTom

1
นอกจากนี้ฉันเพิ่มcontinueในบล็อก catch และbreakหลังบล็อก catch เพื่อให้ลูปสิ้นสุดลงเมื่อการคำนวณทำงาน (ขอบคุณ
Lirik

6
+1 เท่านั้นเพราะมีข้อความว่า "Don't use exceptions for control flow" แม้ว่าฉันจะใช้ "ไม่เคย" แทน "เท่าที่จะทำได้"
Bill K

1
@jjoelson: ข้อความbreakต่อไปนี้calc();ในtry(และไม่มีcontinueคำสั่งเลย) อาจจะเป็นสำนวนมากกว่านี้เล็กน้อย
Adam Robinson

38

เพียงเพื่อเสนอทางเลือก "นอกกรอบ" แล้วฟังก์ชันเรียกซ้ำ ...

//Calling Code
double result = DoCalc();

double DoCalc(int c = 1)
{
   try{
      switch(c){
         case 1: return Calc1();
         case 2: return Calc2();
         case 3: return Calc3();
         default: return CalcDefault();  //default should not be one of the Calcs - infinite loop
      }
   }
   catch{
      return DoCalc(++c);
   }
}

หมายเหตุ: ฉันไม่ได้บอกว่านี่เป็นวิธีที่ดีที่สุดในการทำงานให้เสร็จ แต่เป็นวิธีที่แตกต่างออกไป


6
ฉันต้องใช้ "On Error Resume Next" ในภาษาหนึ่งครั้งและรหัสที่ฉันสร้างขึ้นมีลักษณะเช่นนี้
Jacob Krall

4
โปรดอย่าใช้คำสั่ง switch เพื่อสร้าง for loop
Jeff Ferland

3
ไม่สามารถรักษาได้ที่จะมีคำสั่งสลับสำหรับการวนซ้ำ
Mohamed Abed

1
ฉันรู้ว่าคำตอบของฉันไม่ใช่รหัสที่มีประสิทธิภาพสูงสุด แต่การใช้บล็อก try / catch อีกครั้งสำหรับสิ่งประเภทนี้ไม่ใช่วิธีที่ดีที่สุด น่าเสียดายที่ OP ใช้ไลบรารีของบุคคลที่สามและต้องพยายามอย่างดีที่สุดเท่าที่จะทำได้เพื่อให้ประสบความสำเร็จ ตามหลักการแล้วอินพุตสามารถทำได้โดยการตรวจสอบความถูกต้องก่อนและเลือกฟังก์ชั่นการคำนวณที่ถูกต้องเพื่อให้แน่ใจว่าจะไม่ล้มเหลวแน่นอนคุณสามารถลอง / จับทั้งหมดเพื่อความปลอดภัย;)
musefan

1
return DoCalc(c++)เทียบเท่ากับreturn DoCalc(c)- ค่าที่เพิ่มขึ้นภายหลังจะไม่ถูกส่งผ่านไปลึกกว่านี้ เพื่อให้มันใช้งานได้ (และแนะนำความคลุมเครือมากขึ้น) อาจเป็นเช่นreturn DoCalc((c++,c))นั้น
Artur Czajka

37

คุณสามารถทำให้รังแบนราบได้โดยใส่ลงในวิธีการดังนี้:

private double calcStuff()
{
  try { return calc1(); }
  catch (Calc1Exception e1)
  {
    // Continue on to the code below
  }

  try { return calc2(); }
  catch (Calc2Exception e1)
  {
    // Continue on to the code below
  }

  try { return calc3(); }
  catch (Calc3Exception e1)
  {
    // Continue on to the code below
  }

  throw new NoCalcsWorkedException();
}

แต่ฉันสงสัยว่าปัญหาการออกแบบที่แท้จริงคือการมีอยู่ของวิธีการที่แตกต่างกันสามวิธีที่ทำสิ่งเดียวกันเป็นหลัก (จากมุมมองของผู้โทร) แต่มีข้อยกเว้นที่แตกต่างกันและไม่เกี่ยวข้องกัน

นี้คือสมมติข้อยกเว้นสามเป็นที่ไม่เกี่ยวข้อง หากพวกเขาทั้งหมดมีคลาสพื้นฐานร่วมกันควรใช้ลูปกับบล็อกเดียวตามที่ Ani แนะนำ


1
+1: นี่เป็นวิธีแก้ปัญหาที่สะอาดและไร้สาระที่สุด วิธีแก้ปัญหาอื่น ๆ ที่ฉันเห็นที่นี่พยายามทำให้น่ารัก IMO ตามที่ OP กล่าวเขาไม่ได้เขียน API ดังนั้นเขาจึงติดอยู่กับข้อยกเว้นที่ถูกโยนทิ้งไป
Nate CK

19

พยายามอย่าควบคุมตรรกะโดยอาศัยข้อยกเว้น โปรดทราบด้วยว่าข้อยกเว้นควรถูกโยนทิ้งในกรณีพิเศษเท่านั้น การคำนวณในกรณีส่วนใหญ่ไม่ควรทำให้เกิดข้อยกเว้นเว้นแต่จะเข้าถึงทรัพยากรภายนอกหรือแยกวิเคราะห์สตริงหรือบางสิ่งบางอย่าง อย่างไรก็ตามในกรณีที่เลวร้ายที่สุดให้ทำตามสไตล์ TryMethod (เช่น TryParse ()) เพื่อห่อหุ้มลอจิกข้อยกเว้นและทำให้ขั้นตอนการควบคุมของคุณสามารถบำรุงรักษาได้และสะอาด:

bool TryCalculate(out double paramOut)
{
  try
  {
    // do some calculations
    return true;
  }
  catch(Exception e)
  { 
     // do some handling
    return false;
  }

}

double calcOutput;
if(!TryCalc1(inputParam, out calcOutput))
  TryCalc2(inputParam, out calcOutput);

รูปแบบอื่นที่ใช้รูปแบบลองและรวมรายการวิธีการแทนการซ้อนกันหาก:

internal delegate bool TryCalculation(out double output);

TryCalculation[] tryCalcs = { calc1, calc2, calc3 };

double calcOutput;
foreach (var tryCalc in tryCalcs.Where(tryCalc => tryCalc(out calcOutput)))
  break;

และถ้า foreach ซับซ้อนเล็กน้อยคุณสามารถทำให้มันธรรมดาได้:

        foreach (var tryCalc in tryCalcs)
        {
            if (tryCalc(out calcOutput)) break;
        }

จริงๆแล้วฉันคิดว่าสิ่งนี้ทำให้เกิดนามธรรมที่ไม่จำเป็น นี่ไม่ใช่วิธีแก้ปัญหาที่น่ากลัว แต่ฉันจะไม่ใช้วิธีนี้ในกรณีส่วนใหญ่
user606723

หากคุณไม่สนใจเกี่ยวกับประเภทข้อยกเว้นและคุณเพียงแค่ต้องการจัดการกับโค้ดเงื่อนไข .. ดังนั้นจึงเป็นการดีกว่าในแง่ของนามธรรมและความสามารถในการบำรุงรักษาที่จะแปลงเป็นวิธีเงื่อนไขพร้อมผลตอบแทนไม่ว่าจะสำเร็จหรือไม่ด้วยวิธีนี้ คุณซ่อนข้อยกเว้นในการจัดการไวยากรณ์ที่ยุ่งเหยิงด้วยวิธีการอธิบายที่ชัดเจน .. จากนั้นโค้ดของคุณจะจัดการกับมันตามที่เป็นวิธีการตามเงื่อนไขปกติ
Mohamed Abed

ฉันรู้ว่าคะแนนและมันถูกต้อง อย่างไรก็ตามเมื่อใช้สิ่งที่เป็นนามธรรมประเภทนี้ (ซ่อนความยุ่งเหยิง / ความซับซ้อน) เกือบทุกที่มันจะกลายเป็นเรื่องไร้สาระและการทำความเข้าใจกับซอฟต์แวร์ว่ามันยากกว่ามาก อย่างที่บอกไม่ใช่วิธีแก้ปัญหาที่น่ากลัว แต่จะไม่ใช้มันเบา
user606723

9

สร้างรายชื่อผู้รับมอบสิทธิ์สำหรับฟังก์ชันการคำนวณของคุณจากนั้นมี while loop เพื่อวนรอบพวกเขา:

List<Func<double>> calcMethods = new List<Func<double>>();

// Note: I haven't done this in a while, so I'm not sure if
// this is the correct syntax for Func delegates, but it should
// give you an idea of how to do this.
calcMethods.Add(new Func<double>(calc1));
calcMethods.Add(new Func<double>(calc2));
calcMethods.Add(new Func<double>(calc3));

double val;
for(CalcMethod calc in calcMethods)
{
    try
    {
        val = calc();
        // If you didn't catch an exception, then break out of the loop
        break;
    }
    catch(GenericCalcException e)
    {
        // Not sure what your exception would be, but catch it and continue
    }

}

return val; // are you returning the value?

นั่นควรให้แนวคิดทั่วไปเกี่ยวกับวิธีการทำ (กล่าวคือไม่ใช่วิธีแก้ปัญหาที่แน่นอน)


1
ยกเว้นแน่นอนสำหรับข้อเท็จจริงที่ว่าปกติแล้วคุณไม่ควรจับExceptionได้ ;)
DeCaf

@DeCaf อย่างที่บอกว่า "ควรให้แนวคิดทั่วไปเกี่ยวกับวิธีการทำ (เช่นไม่ใช่วิธีแก้ปัญหาที่แน่นอน)" ดังนั้น OP สามารถจับสิ่งที่ยกเว้นที่เหมาะสมที่จะเกิดขึ้น ... Exceptionไม่จำเป็นต้องจับทั่วไป
Kiril

ใช่ขอโทษแค่รู้สึกว่าต้องเอามันออกไป
DeCaf

1
@DeCaf เป็นคำชี้แจงที่ถูกต้องสำหรับผู้ที่อาจไม่คุ้นเคยกับแนวทางปฏิบัติที่ดีที่สุด ขอบคุณ :)
Kiril

9

ดูเหมือนงานสำหรับ ... MONADS! โดยเฉพาะบางที monad เริ่มต้นด้วยอาจจะ monadตามที่อธิบายไว้ที่นี่ จากนั้นเพิ่มวิธีการขยาย ฉันเขียนวิธีการขยายเหล่านี้โดยเฉพาะสำหรับปัญหาตามที่คุณอธิบายไว้ สิ่งที่ดีเกี่ยวกับ monads คือคุณสามารถเขียนวิธีการขยายที่จำเป็นสำหรับสถานการณ์ของคุณได้

public static Maybe<T> TryGet<T>(this Maybe<T> m, Func<T> getFunction)
{
    // If m has a value, just return m - we want to return the value
    // of the *first* successful TryGet.
    if (m.HasValue)
    {
        return m;
    }

    try
    {
        var value = getFunction();

        // We were able to successfully get a value. Wrap it in a Maybe
        // so that we can continue to chain.
        return value.ToMaybe();
    }
    catch
    {
        // We were unable to get a value. There's nothing else we can do.
        // Hopefully, another TryGet or ThrowIfNone will handle the None.
        return Maybe<T>.None;
    }
}

public static Maybe<T> ThrowIfNone<T>(
    this Maybe<T> m,
    Func<Exception> throwFunction)
{
    if (!m.HasValue)
    {
        // If m does not have a value by now, give up and throw.
        throw throwFunction();
    }

    // Otherwise, pass it on - someone else should unwrap the Maybe and
    // use its value.
    return m;
}

ใช้มันดังนี้:

[Test]
public void ThrowIfNone_ThrowsTheSpecifiedException_GivenNoSuccessfulTryGet()
{
    Assert.That(() =>
        Maybe<double>.None
            .TryGet(() => { throw new Exception(); })
            .TryGet(() => { throw new Exception(); })
            .TryGet(() => { throw new Exception(); })
            .ThrowIfNone(() => new NoCalcsWorkedException())
            .Value,
        Throws.TypeOf<NoCalcsWorkedException>());
}

[Test]
public void Value_ReturnsTheValueOfTheFirstSuccessfulTryGet()
{
    Assert.That(
        Maybe<double>.None
            .TryGet(() => { throw new Exception(); })
            .TryGet(() => 0)
            .TryGet(() => 1)
            .ThrowIfNone(() => new NoCalcsWorkedException())
            .Value,
        Is.EqualTo(0));
}

หากคุณพบว่าตัวเองทำการคำนวณประเภทนี้บ่อยครั้ง monad อาจลดจำนวนโค้ดสำเร็จรูปที่คุณต้องเขียนในขณะที่เพิ่มความสามารถในการอ่านโค้ดของคุณ


2
ฉันชอบวิธีนี้ อย่างไรก็ตามมันค่อนข้างทึบสำหรับทุกคนที่ไม่เคยสัมผัสกับ monads มาก่อนซึ่งหมายความว่านี่ยังห่างไกลจากสำนวนใน c # ฉันไม่ต้องการให้เพื่อนร่วมงานคนหนึ่งของฉันได้เรียนรู้สิ่งต่างๆเพียงเพื่อแก้ไขโค้ดโง่ ๆ ชิ้นนี้ในอนาคต อย่างไรก็ตามนี่เป็นสิ่งที่ดีสำหรับการอ้างอิงในอนาคต
jjoelson

1
+1 สำหรับความรู้สึกตลกสำหรับการเขียนวิธีการแก้ปัญหาที่ป้านและละเอียดที่สุดเท่าที่จะเป็นไปได้สำหรับปัญหานี้จากนั้นบอกว่าจะ "ลดจำนวนโค้ดสำเร็จรูปที่คุณต้องเขียนในขณะที่เพิ่มความสามารถในการอ่านโค้ดของคุณ"
Nate CK

1
เฮ้เราไม่ได้บ่นเกี่ยวกับโค้ดจำนวนมากที่ซ่อนอยู่ใน System.Linq และใช้ monads เหล่านั้นอย่างมีความสุขตลอดทั้งวัน ฉันคิดว่า @ fre0n แค่หมายความว่าถ้าคุณเต็มใจที่จะใส่บางที monad ในชุดเครื่องมือของคุณการประเมินแบบล่ามโซ่เหล่านี้จะง่ายต่อการมองและให้เหตุผล มีเครื่องมือมากมายที่ง่ายต่อการคว้า
Sebastian Good

เพียงเพราะการใช้Maybeไม่ได้ทำให้วิธีนี้เป็นวิธีแก้ปัญหาแบบ monadic จะใช้เป็นศูนย์ของคุณสมบัติของเอกMaybeและอื่น ๆ nullอาจรวมทั้งใช้เพียง นอกจากนี้ใช้ "monadicly" นี่จะเป็นสิ่งที่ตรงกันข้ามของMaybe. วิธีแก้ปัญหาแบบ monadic จริงจะต้องใช้ State monad ที่เก็บค่าที่ไม่โดดเด่นเป็นอันดับแรกเป็นสถานะของมัน แต่นั่นจะมากเกินไปเมื่อการประเมินแบบล่ามโซ่ตามปกติทำงานได้
Dax Fohl

7

วิธีการลองรุ่นอื่น อันนี้อนุญาตให้มีการพิมพ์ยกเว้นเนื่องจากมีประเภทข้อยกเว้นสำหรับการคำนวณแต่ละครั้ง:

    public bool Try<T>(Func<double> func, out double d) where T : Exception
    {
      try
      {
        d = func();
        return true;
      }
      catch (T)
      {
        d = 0;
        return false;
      }
    }

    // usage:
    double d;
    if (!Try<Calc1Exception>(() = calc1(), out d) && 
        !Try<Calc2Exception>(() = calc2(), out d) && 
        !Try<Calc3Exception>(() = calc3(), out d))

      throw new NoCalcsWorkedException();
    }

คุณสามารถหลีกเลี่ยง ifs ที่ซ้อนกันได้โดยใช้&&ระหว่างแต่ละเงื่อนไขแทน
DeCaf

4

ใน Perl คุณสามารถทำได้foo() or bar()ซึ่งจะดำเนินการbar()หากfoo()ล้มเหลว ใน C # เราไม่เห็นสิ่งนี้สร้าง "ถ้าล้มเหลวแล้ว" แต่มีตัวดำเนินการที่เราสามารถใช้เพื่อจุดประสงค์นี้: ตัวดำเนินการรวมกันเป็นโมฆะ??ซึ่งจะดำเนินต่อไปก็ต่อเมื่อส่วนแรกเป็นโมฆะ

หากคุณสามารถเปลี่ยนลายเซ็นของการคำนวณของคุณและหากคุณตัดข้อยกเว้น (ดังที่แสดงในโพสต์ก่อนหน้านี้) หรือเขียนใหม่เพื่อส่งคืนnullแทนโค้ดเชนของคุณจะสั้นขึ้นเรื่อย ๆ และยังอ่านง่าย:

double? val = Calc1() ?? Calc2() ?? Calc3() ?? Calc4();
if(!val.HasValue) 
    throw new NoCalcsWorkedException();

ฉันใช้ทดแทนต่อไปนี้สำหรับฟังก์ชั่นของคุณซึ่งส่งผลให้ค่าใน40.40val

static double? Calc1() { return null; /* failed */}
static double? Calc2() { return null; /* failed */}
static double? Calc3() { return null; /* failed */}
static double? Calc4() { return 40.40; /* success! */}

ฉันตระหนักดีว่าโซลูชันนี้ใช้ไม่ได้เสมอไป แต่คุณตั้งคำถามที่น่าสนใจมากและฉันเชื่อว่าแม้ว่าเธรดจะค่อนข้างเก่า แต่นี่เป็นรูปแบบที่ควรค่าแก่การพิจารณาเมื่อคุณสามารถทำการแก้ไขได้


1
ฉันแค่อยากจะพูดว่า "ขอบคุณ" ฉันพยายามนำสิ่งที่คุณพูดถึงไปใช้ หวังว่าคงเข้าใจถูกนะ
AlexMelw

3

เนื่องจากวิธีการคำนวณมีลายเซ็นที่ไม่มีพารามิเตอร์เหมือนกันคุณสามารถลงทะเบียนในรายการและวนซ้ำผ่านรายการนั้นและดำเนินการตามวิธีการ อาจจะเป็นการดีกว่าหากคุณใช้Func<double>ความหมายว่า "ฟังก์ชันที่ส่งคืนผลลัพธ์เป็นประเภทdouble"

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
  class CalculationException : Exception { }
  class Program
  {
    static double Calc1() { throw new CalculationException(); }
    static double Calc2() { throw new CalculationException(); }
    static double Calc3() { return 42.0; }

    static void Main(string[] args)
    {
      var methods = new List<Func<double>> {
        new Func<double>(Calc1),
        new Func<double>(Calc2),
        new Func<double>(Calc3)
    };

    double? result = null;
    foreach (var method in methods)
    {
      try {
        result = method();
        break;
      }
      catch (CalculationException ex) {
        // handle exception
      }
     }
     Console.WriteLine(result.Value);
   }
}

3

คุณสามารถใช้ Task / ContinueWith และตรวจสอบข้อยกเว้น นี่เป็นวิธีการขยายที่ดีที่จะช่วยทำให้สวย:

    static void Main() {
        var task = Task<double>.Factory.StartNew(Calc1)
            .OrIfException(Calc2)
            .OrIfException(Calc3)
            .OrIfException(Calc4);
        Console.WriteLine(task.Result); // shows "3" (the first one that passed)
    }

    static double Calc1() {
        throw new InvalidOperationException();
    }

    static double Calc2() {
        throw new InvalidOperationException();
    }

    static double Calc3() {
        return 3;
    }

    static double Calc4() {
        return 4;
    }
}

static class A {
    public static Task<T> OrIfException<T>(this Task<T> task, Func<T> nextOption) {
        return task.ContinueWith(t => t.Exception == null ? t.Result : nextOption(), TaskContinuationOptions.ExecuteSynchronously);
    }
}

1

หากประเภทของข้อยกเว้นที่เกิดขึ้นจริงไม่สำคัญคุณสามารถใช้บล็อกจับแบบไม่ระบุประเภท:

var setters = new[] { calc1, calc2, calc3 };
bool succeeded = false;
foreach(var s in setters)
{
    try
    {
            val = s();
            succeeded = true;
            break;
    }
    catch { /* continue */ }
}
if (!suceeded) throw new NoCalcsWorkedException();

ไม่เรียกทุกฟังก์ชันในรายการเสมอไปใช่หรือไม่? อาจต้องการโยน (เล่นสำนวนไม่ได้ตั้งใจ) ในบางสิ่งเช่นif(succeeded) { break; }โพสต์แคป
CVn

1
using System;

namespace Utility
{
    /// <summary>
    /// A helper class for try-catch-related functionality
    /// </summary>
    public static class TryHelper
    {
        /// <summary>
        /// Runs each function in sequence until one throws no exceptions;
        /// if every provided function fails, the exception thrown by
        /// the final one is left unhandled
        /// </summary>
        public static void TryUntilSuccessful( params Action[] functions )
        {
            Exception exception = null;

            foreach( Action function in functions )
            {
                try
                {
                    function();
                    return;
                }
                catch( Exception e )
                {
                    exception   = e;
                }
            }

            throw exception;
        }
    }
}

และใช้มันดังนี้:

using Utility;

...

TryHelper.TryUntilSuccessful(
    () =>
    {
        /* some code */
    },
    () =>
    {
        /* more code */
    },
    calc1,
    calc2,
    calc3,
    () =>
    {
        throw NotImplementedException();
    },
    ...
);

1

ดูเหมือนว่าความตั้งใจของ OP คือการค้นหารูปแบบที่ดีในการแก้ปัญหาของเขาและแก้ไขปัญหาปัจจุบันที่เขากำลังดิ้นรนอยู่ในขณะนั้น

OP: "ฉันสามารถรวมการคำนวณแต่ละรายการด้วยวิธีตัวช่วยซึ่งส่งคืนค่าว่างเมื่อเกิดความล้มเหลวจากนั้นใช้ตัว??ดำเนินการ แต่มีวิธีการทำเช่นนี้โดยทั่วไปหรือไม่ (กล่าวคือโดยไม่ต้องเขียนวิธีการช่วยเหลือสำหรับแต่ละวิธีที่ฉันต้องการ ใช้)? ฉันเคยคิดเกี่ยวกับการเขียนวิธีการคงที่โดยใช้ยาสามัญซึ่งรวมวิธีการใด ๆ ในการลอง / จับและส่งคืนค่าว่างเมื่อเกิดความล้มเหลว แต่ฉันไม่แน่ใจว่าจะทำอย่างไรกับสิ่งนี้มีความคิดอย่างไร "

ฉันเห็นรูปแบบที่ดีมากมายที่หลีกเลี่ยงการลองจับบล็อกที่ซ้อนกันโพสต์ในฟีดนี้ แต่ไม่พบวิธีแก้ปัญหาที่อ้างถึงข้างต้น ดังนั้นนี่คือวิธีแก้ปัญหา:

ในฐานะที่เป็น OP กล่าวเขาต้องการที่จะทำให้วัตถุที่ห่อหุ้มซึ่งผลตอบแทนnullในความล้มเหลว ฉันจะเรียกมันว่าพ็อด ( Exception-safe pod )

public static void Run()
{
    // The general case
    // var safePod1 = SafePod.CreateForValueTypeResult(() => CalcX(5, "abc", obj));
    // var safePod2 = SafePod.CreateForValueTypeResult(() => CalcY("abc", obj));
    // var safePod3 = SafePod.CreateForValueTypeResult(() => CalcZ());

    // If you have parameterless functions/methods, you could simplify it to:
    var safePod1 = SafePod.CreateForValueTypeResult(Calc1);
    var safePod2 = SafePod.CreateForValueTypeResult(Calc2);
    var safePod3 = SafePod.CreateForValueTypeResult(Calc3);

    var w = safePod1() ??
            safePod2() ??
            safePod3() ??
            throw new NoCalcsWorkedException(); // I've tested it on C# 7.2

    Console.Out.WriteLine($"result = {w}"); // w = 2.000001
}

private static double Calc1() => throw new Exception("Intentionally thrown exception");
private static double Calc2() => 2.000001;
private static double Calc3() => 3.000001;

แต่ถ้าคุณต้องการสร้างพ็อดที่ปลอดภัยสำหรับผลลัพธ์ประเภทการอ้างอิงที่ส่งคืนโดยฟังก์ชัน / วิธีการ CalcN ()

public static void Run()
{
    var safePod1 = SafePod.CreateForReferenceTypeResult(Calc1);
    var safePod2 = SafePod.CreateForReferenceTypeResult(Calc2);
    var safePod3 = SafePod.CreateForReferenceTypeResult(Calc3);

    User w = safePod1() ?? safePod2() ?? safePod3();

    if (w == null) throw new NoCalcsWorkedException();

    Console.Out.WriteLine($"The user object is {{{w}}}"); // The user object is {Name: Mike}
}

private static User Calc1() => throw new Exception("Intentionally thrown exception");
private static User Calc2() => new User { Name = "Mike" };
private static User Calc3() => new User { Name = "Alex" };

class User
{
    public string Name { get; set; }
    public override string ToString() => $"{nameof(Name)}: {Name}";
}

ดังนั้นคุณอาจสังเกตเห็นว่าไม่มีความจำเป็น"ที่จะเขียนวิธีการช่วยเหลือสำหรับแต่ละวิธีที่คุณต้องการใช้งาน"

สองประเภทของฝัก (สำหรับValueTypeResultและReferenceTypeResults) มีความเพียงพอ


นี่คือรหัสของSafePod. มันไม่ใช่ภาชนะ แต่จะสร้างกระดาษห่อผู้ร่วมประชุมที่ปลอดภัยเป็นพิเศษสำหรับทั้งValueTypeResults และReferenceTypeResults

public static class SafePod
{
    public static Func<TResult?> CreateForValueTypeResult<TResult>(Func<TResult> jobUnit) where TResult : struct
    {
        Func<TResult?> wrapperFunc = () =>
        {
            try { return jobUnit.Invoke(); } catch { return null; }
        };

        return wrapperFunc;
    }

    public static Func<TResult> CreateForReferenceTypeResult<TResult>(Func<TResult> jobUnit) where TResult : class
    {
        Func<TResult> wrapperFunc = () =>
        {
            try { return jobUnit.Invoke(); } catch { return null; }
        };

        return wrapperFunc;
    }
}

นั่นคือวิธีที่คุณสามารถใช้ประโยชน์จากตัวดำเนินการที่??รวมกันเป็นโมฆะรวมกับพลังของเอนทิตีพลเมืองชั้นdelegateหนึ่ง


0

คุณพูดถูกเกี่ยวกับการคำนวณแต่ละครั้ง แต่คุณควรสรุปตามหลักการบอกไม่ถาม

double calc3WithConvertedException(){
    try { val = calc3(); }
    catch (Calc3Exception e3)
    {
        throw new NoCalcsWorkedException();
    }
}

double calc2DefaultingToCalc3WithConvertedException(){
    try { val = calc2(); }
    catch (Calc2Exception e2)
    {
        //defaulting to simpler method
        return calc3WithConvertedException();
    }
}


double calc1DefaultingToCalc2(){
    try { val = calc2(); }
    catch (Calc1Exception e1)
    {
        //defaulting to simpler method
        return calc2defaultingToCalc3WithConvertedException();
    }
}

การดำเนินการนั้นง่ายและสามารถเปลี่ยนพฤติกรรมได้อย่างอิสระ และไม่สำคัญว่าทำไมพวกเขาถึงผิดนัด เพื่อพิสูจน์ว่าคุณสามารถใช้ calc1DefaultingToCalc2 เป็น:

double calc1DefaultingToCalc2(){
    try { 
        val = calc2(); 
        if(specialValue(val)){
            val = calc2DefaultingToCalc3WithConvertedException()
        }
    }
    catch (Calc1Exception e1)
    {
        //defaulting to simpler method
        return calc2defaultingToCalc3WithConvertedException();
    }
}

-1

ดูเหมือนว่าการคำนวณของคุณจะมีข้อมูลที่ถูกต้องมากกว่าการคำนวณ บางทีมันอาจจะสมเหตุสมผลกว่าสำหรับพวกเขาในการจัดการข้อยกเว้นของตนเองและส่งคืนคลาส "ผลลัพธ์" ที่มีข้อมูลข้อผิดพลาดข้อมูลค่า ฯลฯ ให้คิดว่าคลาส AsyncResult ทำตามรูปแบบ async จากนั้นคุณสามารถประเมินผลลัพธ์ที่แท้จริงของการคำนวณได้ คุณสามารถหาเหตุผลเข้าข้างตนเองได้โดยคิดในแง่ที่ว่าหากการคำนวณล้มเหลวนั่นเป็นเพียงข้อมูลราวกับว่ามันผ่านไป ดังนั้นข้อยกเว้นคือข้อมูลส่วนหนึ่งไม่ใช่ "ข้อผิดพลาด"

internal class SomeCalculationResult 
{ 
     internal double? Result { get; private set; } 
     internal Exception Exception { get; private set; }
}

...

SomeCalculationResult calcResult = Calc1();
if (!calcResult.Result.HasValue) calcResult = Calc2();
if (!calcResult.Result.HasValue) calcResult = Calc3();
if (!calcResult.Result.HasValue) throw new NoCalcsWorkedException();

// do work with calcResult.Result.Value

...

แน่นอนฉันสงสัยมากขึ้นเกี่ยวกับสถาปัตยกรรมโดยรวมที่คุณใช้ในการคำนวณเหล่านี้ให้เสร็จสิ้น


สิ่งนี้ใช้ได้ - คล้ายกับที่ OP แนะนำเท่าที่รวมการคำนวณ ฉันต้องการสิ่งที่ชอบwhile (!calcResult.HasValue) nextCalcResult()แทนที่จะเป็นรายการ Calc1, Calc2, Calc3 เป็นต้น
Kirk Broadhurst

-3

แล้วการติดตามการกระทำของคุณ ...

double val;
string track = string.Empty;

try 
{ 
  track = "Calc1";
  val = calc1(); 

  track = "Calc2";
  val = calc2(); 

  track = "Calc3";
  val = calc3(); 
}
catch (Exception e3)
{
   throw new NoCalcsWorkedException( track );
}

4
วิธีนี้ช่วยได้อย่างไร? ถ้า calc1 () ล้มเหลว cals2 จะไม่ถูกดำเนินการ!
DeCaf

สิ่งนี้ไม่สามารถแก้ปัญหาได้ ดำเนินการ calc1 เฉพาะในกรณีที่ calc2 ล้มเหลวให้ดำเนินการ calc3 หาก calc1 && calc2 ล้มเหลวเท่านั้น
Jason

+1 หรือ นี่คือสิ่งที่ฉันทำ ฉันมีเพียงแค่รหัสหนึ่งจับข้อความที่ส่งถึงฉัน ( trackในกรณีนี้) และฉันรู้ว่าสิ่งที่ส่วนในรหัสของฉันที่เกิดจากบล็อกที่จะล้มเหลว บางทีคุณควรอธิบายอย่างละเอียดเพื่อบอกสมาชิกเช่น DeCaf ว่าtrackข้อความถูกส่งไปยังรูทีนการจัดการข้อผิดพลาดที่กำหนดเองซึ่งช่วยให้คุณสามารถดีบักโค้ดของคุณได้ ดูเหมือนว่าเขาไม่เข้าใจตรรกะของคุณ
jp2code

ดี @DeCaf ถูกต้องส่วนรหัสของฉันไม่ดำเนินการฟังก์ชั่นถัดไปซึ่งเป็นสิ่งที่ jjoelson ขอมีวิธีแก้ปัญหาของฉันไม่สามารถทำได้
Orn Kristjansson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.