เป็นไปได้ไหมที่จะเขียนข้อความยืนยันมากเกินไป?


33

ฉันเป็นแฟนตัวยงของการassertตรวจสอบการเขียนในรหัส C ++ เป็นวิธีการตรวจสอบกรณีในระหว่างการพัฒนาที่ไม่สามารถเกิดขึ้นได้ แต่จะเกิดขึ้นเพราะข้อบกพร่องตรรกะในโปรแกรมของฉัน เป็นการปฏิบัติที่ดีโดยทั่วไป

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

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

; gray out the "assert(...)" wrapper
(add-hook 'c-mode-common-hook
  (lambda () (font-lock-add-keywords nil
    '(("\\<\\(assert\(.*\);\\)" 1 '(:foreground "#444444") t)))))

; gray out the stuff inside parenthesis with a slightly lighter color
(add-hook 'c-mode-common-hook
  (lambda () (font-lock-add-keywords nil
    '(("\\<assert\\(\(.*\);\\)" 1 '(:foreground "#666666") t)))))

3
ฉันต้องยอมรับว่านี่เป็นคำถามที่ข้ามความคิดของฉันไปเรื่อย ๆ สนใจฟังความคิดเห็นของผู้อื่นในเรื่องนี้
Captain Sensible

คำตอบ:


45

ผมเคยเห็นหลายร้อยข้อบกพร่องที่จะได้รับการแก้ไขได้เร็วขึ้นถ้ามีคนเขียนมากขึ้นอ้างและไม่ได้เป็นหนึ่งเดียวที่จะได้รับการแก้ไขได้เร็วขึ้นโดยการเขียนน้อยลง

[C] อาจเป็นแบบฝึกหัดการเขียนโปรแกรมที่ไม่ดีในแง่ของความสามารถในการอ่านและการบำรุงรักษา [?]

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

นอกจากนี้ในประสบการณ์ของผมการบำรุงรักษาเป็นเสมอดีขึ้นโดยการอ้างเช่นเดียวกับที่มันเป็นโดยการทดสอบหน่วย Asserts จัดเตรียมการตรวจสอบสติที่ใช้รหัสตามที่ตั้งใจจะใช้


1
คำตอบที่ดี. ฉันยังเพิ่มคำอธิบายให้กับคำถามว่าฉันจะปรับปรุงการอ่านด้วย Emacs ได้อย่างไร
อลันทัวริง

2
"เป็นประสบการณ์ของฉันที่คนที่เขียนข้อความยืนยันที่ดีก็เขียนโค้ดที่อ่านได้" << ดีเยี่ยม การสร้างโค้ดที่อ่านได้นั้นขึ้นอยู่กับโปรแกรมเมอร์แต่ละคนเพราะเป็นเทคนิคที่เขาหรือเธอใช้และไม่ได้รับอนุญาตให้ใช้ ฉันเคยเห็นเทคนิคที่ดีกลายเป็นอ่านไม่ได้ในมือผิดและแม้กระทั่งสิ่งที่ส่วนใหญ่จะพิจารณาเทคนิคที่ไม่ดีกลายเป็นที่ชัดเจนอย่างสมบูรณ์แบบแม้สง่างามโดยใช้สิ่งที่เป็นนามธรรมและแสดงความคิดเห็น
Greg Jackson

ฉันมีแอปพลิเคชันขัดข้องสองสามตัวที่เกิดจากการยืนยันที่ผิดพลาด ดังนั้นฉันจึงเห็นข้อบกพร่องที่ไม่มีตัวตนถ้ามีคน (ตัวเอง) เขียนข้อความยืนยันน้อยลง
CodesInChaos

@CodesInChaos เนื้อหาที่พิมพ์ผิดชี้ไปที่ข้อผิดพลาดในการกำหนดปัญหา - นั่นคือข้อผิดพลาดในการออกแบบจึงไม่ตรงกันระหว่างการยืนยันและรหัส (อื่น ๆ )
ลอเรนซ์

12

เป็นไปได้ไหมที่จะเขียนข้อความยืนยันมากเกินไป?

แน่นอนมันเป็น [ลองนึกภาพตัวอย่างที่น่ารังเกียจที่นี่] อย่างไรก็ตามการใช้แนวทางที่มีรายละเอียดดังต่อไปนี้คุณไม่ควรมีปัญหาในการผลักดันข้อ จำกัด ดังกล่าวในทางปฏิบัติ ฉันเป็นแฟนตัวยงของการยืนยันเช่นกันและฉันใช้พวกเขาตามหลักการเหล่านี้ คำแนะนำนี้ส่วนใหญ่ไม่ได้เป็นพิเศษในการยืนยัน แต่เฉพาะการปฏิบัติทางวิศวกรรมที่ดีทั่วไปที่ใช้กับพวกเขา

คำนึงถึงค่าใช้จ่ายในการดำเนินการและฐานรากไบนารี

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

ฉันชอบที่จะวัดค่าใช้จ่ายของการยืนยันที่เกี่ยวข้องกับค่าใช้จ่ายของฟังก์ชั่นที่มีอยู่ในพิจารณาสองตัวอย่างต่อไปนี้

// Precondition:  queue is not empty
// Invariant:     queue is sorted
template <typename T>
const T&
sorted_queue<T>::max() const noexcept
{
  assert(!this->data_.empty());
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  return this->data_.back();
}

ฟังก์ชั่นของตัวเองคือการดำเนินการO (1) แต่การยืนยันบัญชีสำหรับค่าใช้จ่ายO ( n ) ฉันไม่คิดว่าคุณต้องการให้เช็คดังกล่าวใช้งานได้เว้นแต่ในกรณีพิเศษ

นี่คือฟังก์ชั่นอื่นที่มีการยืนยันที่คล้ายกัน

// Requirement:   op : T -> T is monotonic [ie x <= y implies op(x) <= op(y)]
// Invariant:     queue is sorted
// Postcondition: each item x in the queue is replaced by op(x)
template <typename T>
template <typename FuncT>
void
sorted_queue<T>::apply_monotonic_function(FuncT&& op)
{
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  std::transform(std::cbegin(this->data_), std::cend(this->data_),
                 std::begin(this->data_), std::forward<FuncT>(op));
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
}

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

ลองพิจารณาตัวอย่างนี้

// Precondition:  queue is not empty
// Invariant:     queue is sorted
// Postcondition: last element is removed from queue
template <typename T>
void
sorted_queue<T>::pop_back() noexcept
{
  assert(!this->data_.empty());
  return this->data_.pop_back();
}

ในขณะที่หลายคนอาจจะพอใจกับการยืนยันO (1) นี้มากกว่าการยืนยันO ( n ) สองรายการในตัวอย่างก่อนหน้าพวกเขามีความเท่าเทียมทางศีลธรรมในมุมมองของฉัน แต่ละค่าใช้จ่ายเพิ่มตามลำดับความซับซ้อนของฟังก์ชันเอง

ในที่สุดก็มีการยืนยันที่ "ถูกจริงๆ" ซึ่งถูกครอบงำด้วยความซับซ้อนของฟังก์ชั่นที่มีอยู่

// Requirement:   cmp : T x T -> bool is a strict weak ordering
// Precondition:  queue is not empty
// Postcondition: if x is returned, then there is no y in the queue
//                such that cmp(x, y)
template <typename T>
template <typename CmpT>
const T&
sorted_queue<T>::max(CmpT&& cmp) const
{
  assert(!this->data_.empty());
  const auto pos = std::max_element(std::cbegin(this->data_),
                                    std::cend(this->data_),
                                    std::forward<CmpT>(cmp));
  assert(pos != std::cend(this->data_));
  return *pos;
}

ที่นี่เรามีการยืนยันสองO (1) ในฟังก์ชั่นO ( n ) มันอาจจะไม่เป็นปัญหาในการรักษาค่าใช้จ่ายนี้แม้จะอยู่ในรุ่นที่วางจำหน่าย

อย่างไรก็ตามโปรดจำไว้ว่าความซับซ้อนเชิงซีมโทติคนั้นไม่ได้ให้การประเมินที่เพียงพอเสมอไปเพราะในทางปฏิบัติเรามักจะจัดการกับขนาดอินพุตที่ล้อมรอบด้วยปัจจัยคงที่แน่นอนและค่าคงที่ที่ซ่อนอยู่โดย "บิ๊ก - โอ "

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

#define MY_ASSERT_IMPL(COST, CONDITION)                                       \
  (                                                                           \
    ( ((COST) <= (MY_ASSERT_COST_LIMIT)) && !(CONDITION) )                    \
      ? ::my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, # CONDITION) \
      : (void) 0                                                              \
  )

#define MY_ASSERT_LOW(CONDITION)                                              \
  MY_ASSERT_IMPL(MY_ASSERT_COST_LOW, CONDITION)

#define MY_ASSERT_MEDIUM(CONDITION)                                           \
  MY_ASSERT_IMPL(MY_ASSERT_COST_MEDIUM, CONDITION)

#define MY_ASSERT_HIGH(CONDITION)                                             \
  MY_ASSERT_IMPL(MY_ASSERT_COST_HIGH, CONDITION)

#define MY_ASSERT_COST_NONE    0
#define MY_ASSERT_COST_LOW     1
#define MY_ASSERT_COST_MEDIUM  2
#define MY_ASSERT_COST_HIGH    3
#define MY_ASSERT_COST_ALL    10

#ifndef MY_ASSERT_COST_LIMIT
#  define MY_ASSERT_COST_LIMIT MY_ASSERT_COST_MEDIUM
#endif

namespace my
{

  [[noreturn]] extern void
  assertion_failed(const char * filename, int line, const char * function,
                   const char * message) noexcept;

}

ตอนนี้คุณสามารถใช้สามแมโครMY_ASSERT_LOW, MY_ASSERT_MEDIUMและMY_ASSERT_HIGHแทนของไลบรารีมาตรฐานของ“หนึ่งขนาดเหมาะกับทุกคน” assertแมโครสำหรับยืนยันที่ได้รับการครอบงำโดยไม่ถูกครอบงำโดยมิได้มีอำนาจเหนือและมีอำนาจเหนือความซับซ้อนของฟังก์ชั่นที่มีของพวกเขาตามลำดับ เมื่อคุณสร้างซอฟต์แวร์คุณสามารถกำหนดสัญลักษณ์ตัวประมวลผลล่วงหน้าล่วงหน้าMY_ASSERT_COST_LIMITเพื่อเลือกชนิดของการยืนยันที่ควรทำให้เป็นสิ่งที่เรียกใช้งานได้ ค่าคงที่MY_ASSERT_COST_NONEและMY_ASSERT_COST_ALLไม่สอดคล้องกับแมโครยืนยันใด ๆ และมีไว้เพื่อใช้เป็นค่าสำหรับMY_ASSERT_COST_LIMITเพื่อปิดการยืนยันทั้งหมดหรือเปิดตามลำดับ

เราพึ่งพาสมมติฐานที่นี่ว่าคอมไพเลอร์ที่ดีจะไม่สร้างรหัสใด ๆ

if (false_constant_expression && run_time_expression) { /* ... */ }

และแปลง

if (true_constant_expression && run_time_expression) { /* ... */ }

เข้าไป

if (run_time_expression) { /* ... */ }

ซึ่งฉันเชื่อว่าเป็นสมมติฐานที่ปลอดภัยในปัจจุบัน

หากคุณกำลังจะปรับแต่งโค้ดข้างต้นพิจารณาคำอธิบายประกอบคอมไพเลอร์ที่เฉพาะเจาะจงเช่น__attribute__ ((cold))บนmy::assertion_failedหรือ__builtin_expect(…, false)ใน!(CONDITION)การลดค่าใช้จ่ายของการยืนยันผ่าน ในรุ่นที่วางจำหน่ายคุณอาจพิจารณาแทนที่การเรียกใช้ฟังก์ชันเป็นmy::assertion_failedด้วยสิ่งที่ต้องการ__builtin_trapลดการพิมพ์เท้าที่ไม่สะดวกในการสูญเสียข้อความวินิจฉัย

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

เปรียบเทียบวิธีการใช้รหัสนี้

int
positive_difference_1st(const int a, const int b) noexcept
{
  if (!(a > b))
    my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, "!(a > b)");
  return a - b;
}

รวบรวมในการชุมนุมต่อไปนี้

_ZN4test23positive_difference_1stEii:
.LFB0:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L5
        movl    %edi, %eax
        subl    %esi, %eax
        ret
.L5:
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %ecx
        movl    $_ZZN4test23positive_difference_1stEiiE12__FUNCTION__, %edx
        movl    $50, %esi
        movl    $.LC1, %edi
        call    _ZN2my16assertion_failedEPKciS1_S1_
        .cfi_endproc
.LFE0:

ในขณะที่รหัสต่อไปนี้

int
positive_difference_2nd(const int a, const int b) noexcept
{
  if (__builtin_expect(!(a > b), false))
    __builtin_trap();
  return a - b;
}

ให้แอสเซมบลีนี้

_ZN4test23positive_difference_2ndEii:
.LFB1:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L8
        movl    %edi, %eax
        subl    %esi, %eax
        ret
        .p2align 4,,7
        .p2align 3
.L8:
        ud2
        .cfi_endproc
.LFE1:

ซึ่งฉันรู้สึกสะดวกสบายมากขึ้นด้วย (ตัวอย่างถูกทดสอบกับ GCC 5.3.0 โดยใช้-std=c++14, -O3และตั้ง-march=nativeค่าสถานะบน 4.3.3-2-ARCH x86_64 GNU / Linux. ไม่แสดงในตัวอย่างข้างต้นเป็นการประกาศtest::positive_difference_1stและtest::positive_difference_2ndที่ฉันเพิ่มลง__attribute__ ((hot))ไปmy::assertion_failedถูกประกาศด้วย__attribute__ ((cold)).)

ยืนยันเงื่อนไขเบื้องต้นในฟังก์ชั่นที่ขึ้นอยู่กับพวกเขา

สมมติว่าคุณมีฟังก์ชันต่อไปนี้พร้อมสัญญาที่ระบุ

/**
 * @brief
 *         Counts the frequency of a letter in a string.
 *
 * The frequency count is case-insensitive.
 *
 * If `text` does not point to a NUL terminated character array or `letter`
 * is not in the character range `[A-Za-z]`, the behavior is undefined.
 *
 * @param text
 *         text to count the letters in
 *
 * @param letter
 *         letter to count
 *
 * @returns
 *         occurences of `letter` in `text`
 *
 */
std::size_t
count_letters(const char * text, int letter) noexcept;

แทนที่จะเขียน

assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);

ที่แต่ละไซต์การโทรให้วางตรรกะนั้นลงในคำจำกัดความของ count_letters

std::size_t
count_letters(const char *const text, const int letter) noexcept
{
  assert(text != nullptr);
  assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
  auto frequency = std::size_t {};
  // TODO: Figure this out...
  return frequency;
}

และเรียกมันโดยไม่ต้องกังวลใจต่อ

const auto frequency = count_letters(text, letter);

ซึ่งมีข้อดีดังต่อไปนี้

  • คุณต้องเขียนรหัสยืนยันเพียงครั้งเดียว เนื่องจากวัตถุประสงค์ของฟังก์ชั่นนั้นเรียกได้ว่าบ่อยครั้งมากกว่าหนึ่งครั้งสิ่งนี้ควรลดจำนวนassertคำสั่งโดยรวมในโค้ดของคุณ
  • มันเก็บตรรกะที่ตรวจสอบปัจจัยพื้นฐานใกล้กับตรรกะที่ขึ้นอยู่กับพวกเขา ฉันคิดว่านี่เป็นสิ่งสำคัญที่สุด หากลูกค้าของคุณใช้อินเทอร์เฟซของคุณในทางที่ผิดพวกเขาไม่สามารถสันนิษฐานได้ว่าจะใช้การยืนยันที่ถูกต้องอย่างใดอย่างหนึ่งดังนั้นจึงเป็นการดีกว่าที่ฟังก์ชั่นจะบอกพวกเขา

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

ความคิดเดียวกันนี้ใช้กับฟังก์ชั่น“ พิเศษ” เช่นตัวดำเนินการที่ทำงานหนักเกินไป เมื่อฉันเขียนตัววนซ้ำฉันมักจะ - ถ้าธรรมชาติของตัววนซ้ำอนุญาต - ให้ฟังก์ชันสมาชิก

bool
good() const noexcept;

ที่อนุญาตให้ถามว่าปลอดภัยหรือไม่ที่จะตรวจสอบตัววนซ้ำ (แน่นอนในทางปฏิบัติมันเกือบจะเป็นไปได้เสมอเท่านั้นที่จะรับประกันได้ว่ามันจะไม่ปลอดภัยที่จะตรวจสอบซ้ำตัววนซ้ำ แต่ฉันเชื่อว่าคุณยังคงสามารถจับข้อบกพร่องจำนวนมากด้วยฟังก์ชั่นดังกล่าว) แทนที่จะทิ้งรหัสทั้งหมดของฉัน ที่ใช้ตัววนซ้ำกับassert(iter.good())คำสั่งฉันอยากจะใส่assert(this->good())บรรทัดเดียวเป็นบรรทัดแรกoperator*ในการทำให้ตัววนซ้ำ

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

ปัจจัยเงื่อนไขทั่วไปออกมา

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

template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
  assert(is_square(m) && is_symmetric(m));
  // TODO: Somehow decompose that thing...
}

มันจะให้ข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์มากกว่า

cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)

ช่วยได้มากกว่าพูด

detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)

ก่อนอื่นคุณต้องไปดูซอร์สโค้ดในบริบทเพื่อหาว่าอะไรที่ถูกทดสอบจริง

หากคุณมีclassค่าคงที่ที่ไม่น่ารำคาญคุณควรยืนยันกับพวกเขาเป็นครั้งคราวเมื่อคุณยุ่งกับสถานะภายในและต้องการให้แน่ใจว่าคุณออกจากวัตถุในสถานะที่ถูกต้องเมื่อกลับมา

เพื่อจุดประสงค์นี้ผมพบว่ามันมีประโยชน์ในการกำหนดฟังก์ชันสมาชิกที่ฉันอัตภาพเรียกprivate class_invaraiants_hold_สมมติว่าคุณกำลังนำไปใช้อีกครั้งstd::vector(เพราะเราทุกคนรู้ว่ามันไม่ดีพอ) มันอาจมีฟังก์ชั่นเช่นนี้

template <typename T>
bool
vector<T>::class_invariants_hold_() const noexcept
{
  if (this->size_ > this->capacity_)
    return false;
  if ((this->size_ > 0) && (this->data_ == nullptr))
    return false;
  if ((this->capacity_ == 0) != (this->data_ == nullptr))
    return false;
  return true;
}

สังเกตเห็นบางสิ่งเกี่ยวกับเรื่องนี้

  • ฟังก์ชั่นตัวเองเป็นคำกริยาconstและnoexceptสอดคล้องกับแนวทางที่ยืนยันจะไม่มีผลข้างเคียง ถ้ามันสมเหตุสมผลก็จงประกาศconstexprเช่นกัน
  • ภาคแสดงไม่ได้ยืนยันอะไรเลย มันมีความหมายที่จะเรียกว่าภายในassert(this->class_invariants_hold_())ยืนยันเช่น ด้วยวิธีนี้ถ้าการรวบรวมคำยืนยันนั้นเรามั่นใจได้ว่าจะไม่มีค่าใช้จ่ายในการดำเนินการเกิดขึ้น
  • การควบคุมการไหลภายในฟังก์ชั่นนั้นแยกออกเป็นหลายifประโยคด้วยต้นreturns แทนที่จะเป็นนิพจน์ขนาดใหญ่ สิ่งนี้ทำให้ง่ายต่อการก้าวผ่านฟังก์ชั่นในตัวดีบั๊กเกอร์และค้นหาว่าส่วนใดของค่าคงที่ที่ถูกทำลายหากการยืนยันเกิดเพลิงไหม้

อย่าอ้างสิทธิ์ในสิ่งที่โง่

บางสิ่งก็ไม่สมเหตุสมผลที่จะยืนยัน

auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2);  // silly
assert(!numbers.empty());     // silly and redundant

การยืนยันเหล่านี้ไม่ได้ทำให้รหัสแม้แต่น้อยอ่านง่ายขึ้นหรือง่ายขึ้นที่จะให้เหตุผลเกี่ยวกับ โปรแกรมเมอร์ C ++ ทุกคนควรมีความมั่นใจมากพอstd::vectorที่จะแน่ใจได้อย่างไรว่าโค้ดด้านบนนั้นถูกต้องเพียงแค่ดูที่มัน ฉันไม่ได้บอกว่าคุณไม่ควรยืนยันขนาดของภาชนะ หากคุณเพิ่มหรือลบองค์ประกอบที่ใช้โฟลว์การควบคุมที่ไม่น่าสนใจการยืนยันดังกล่าวอาจมีประโยชน์ แต่ถ้าเพียงแค่ทำซ้ำสิ่งที่เขียนในรหัสที่ไม่ใช่การยืนยันข้างต้นจะไม่มีค่าที่ได้รับ

ยังไม่ยืนยันว่าฟังก์ชั่นห้องสมุดทำงานอย่างถูกต้อง

auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled());  // probably silly

หากคุณเชื่อถือห้องสมุดเล็ก ๆ น้อย ๆ ให้พิจารณาใช้ห้องสมุดอื่นแทน

ในทางตรงกันข้ามหากเอกสารของห้องสมุดไม่ชัดเจน 100% และคุณมั่นใจในสัญญาของตนโดยการอ่านซอร์สโค้ดมันสมเหตุสมผลมากที่จะยืนยันว่า“ สัญญาที่อนุมาน” หากห้องสมุดแตกเวอร์ชันอนาคตคุณจะสังเกตเห็นได้อย่างรวดเร็ว

auto w = widget {};
// After reading the source code, I have concluded that quantum mode is
// always off by default but this isn't documented anywhere.
assert(!w.quantum_mode_enabled());

ดีกว่าโซลูชันต่อไปนี้ซึ่งจะไม่บอกคุณว่าสมมติฐานของคุณถูกต้องหรือไม่

auto w = widget {};
if (w.quantum_mode_enabled())
  {
    // I don't think that quantum mode is ever enabled by default but
    // I'm not sure.
    w.disable_quantum_mode();
  }

อย่าละเมิดคำยืนยันในการใช้ตรรกะของโปรแกรม

ควรใช้การยืนยันเพื่อเปิดเผยข้อบกพร่องที่ควรค่าแก่การฆ่าแอปพลิเคชันของคุณในทันที พวกเขาไม่ควรใช้เพื่อตรวจสอบเงื่อนไขอื่น ๆ แม้ว่าปฏิกิริยาที่เหมาะสมกับสภาพนั้นก็จะถูกยกเลิกทันที

ดังนั้นเขียนสิ่งนี้ ...

if (!server_reachable())
  {
    log_message("server not reachable");
    shutdown();
  }

…แทนที่จะเป็นเช่นนั้น

assert(server_reachable());

นอกจากนี้ยังไม่เคยใช้ยืนยันในการตรวจสอบการป้อนข้อมูลที่ไม่น่าเชื่อถือหรือตรวจสอบว่าstd::mallocไม่ได้คุณreturn nullptrแม้ว่าคุณจะรู้ว่าคุณจะไม่ปิดการยืนยันแม้ในรุ่นที่วางจำหน่ายการยืนยันจะสื่อสารกับผู้อ่านว่ามันตรวจสอบบางสิ่งที่เป็นจริงเสมอเนื่องจากโปรแกรมไม่มีข้อบกพร่องและไม่มีผลข้างเคียงที่มองเห็นได้ หากนี่ไม่ใช่ข้อความที่คุณต้องการสื่อสารให้ใช้กลไกการจัดการข้อผิดพลาดทางเลือกเช่นthrowการยกเว้น หากคุณพบว่าสะดวกที่จะมีตัวห่อหุ้มแมโครสำหรับการตรวจสอบการไม่ยืนยันของคุณ อย่าเรียกว่า "ยืนยัน", "สมมติ", "ต้องการ", "มั่นใจ" หรืออะไรทำนองนั้น ตรรกะภายในของมันอาจเหมือนกันassertยกเว้นว่ามันจะไม่ถูกรวบรวมไว้แน่นอน

ข้อมูลมากกว่านี้

ผมพบว่าจอห์น Lakos' พูดคุยโปรแกรมป้องกัน Done Rightรับที่ CppCon'14 ( 1 เซนต์ส่วน , 2 ครั้งส่วนหนึ่ง ) enlightening มาก เขาใช้ความคิดในการปรับแต่งสิ่งที่ยืนยันถูกเปิดใช้งานและวิธีการตอบสนองต่อข้อยกเว้นที่ล้มเหลวยิ่งกว่าที่ฉันทำในคำตอบนี้


4
Assertions are great, but ... you will turn them off sooner or later.- หวังว่าเร็วกว่านี้เช่นก่อนรหัสจัดส่ง สิ่งที่ต้องทำให้โปรแกรมตายในการผลิตควรเป็นส่วนหนึ่งของรหัส "ของจริง" ไม่ใช่การยืนยัน
Blrfl

4

ฉันพบว่าเมื่อเวลาผ่านไปฉันเขียน asserts น้อยลงเพราะพวกเขาจำนวนมากเพื่อ "is the compiler working" และ "is the library working" เมื่อคุณเริ่มคิดเกี่ยวกับสิ่งที่คุณกำลังทดสอบฉันสงสัยว่าคุณจะเขียนข้อความยืนยันน้อยลง

ตัวอย่างเช่นวิธีการ (พูด) เพิ่มบางสิ่งลงในคอลเลกชันไม่จำเป็นต้องยืนยันว่ามีคอลเลกชันอยู่ - โดยทั่วไปแล้วเป็นเงื่อนไขเบื้องต้นของคลาสที่เป็นเจ้าของข้อความหรือเป็นข้อผิดพลาดร้ายแรงที่ควรทำให้ผู้ใช้กลับมาใช้ . ดังนั้นให้ตรวจสอบอีกครั้ง แต่เนิ่นๆแล้วลองคิดดู

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


4
ฉันไม่เข้าใจประเด็นของคุณเมื่อคุณพูดว่า“ โดยทั่วไปแล้วเป็นเงื่อนไขเบื้องต้นของคลาสที่เป็นเจ้าของข้อความหรือเป็นข้อผิดพลาดร้ายแรงที่ควรทำให้ผู้ใช้กลับมาใช้งานอีกครั้ง ดังนั้นให้ลองตรวจสอบอีกครั้ง แต่เนิ่นๆแล้วลองคิดดูสิ”คุณกำลังใช้การยืนยันอะไรหากไม่ตรวจสอบสมมติฐานของคุณ
5gon12eder

4

มีคำยืนยันน้อยเกินไป: ขอให้โชคดีในการเปลี่ยนรหัสที่เต็มไปด้วยสมมติฐานที่ซ่อนอยู่

มีการยืนยันมากเกินไป: สามารถนำไปสู่ปัญหาการอ่านและกลิ่นรหัส - คลาส, ฟังก์ชัน, API ออกแบบมาถูกต้องหรือไม่เมื่อมีข้อสันนิษฐานมากมายในข้อความยืนยัน

อาจมีการยืนยันที่ไม่ได้ตรวจสอบอะไรจริงๆหรือตรวจสอบสิ่งต่าง ๆ เช่นการตั้งค่าคอมไพเลอร์ในแต่ละฟังก์ชั่น: /

เล็งไปที่จุดที่น่ารัก แต่ไม่น้อย (อย่างที่คนอื่นพูดไปแล้วการยืนยันมากขึ้นนั้นมีอันตรายน้อยกว่าการมีน้อยเกินไปหรือพระเจ้าช่วยเราไม่ได้)


3

มันจะยอดเยี่ยมถ้าคุณสามารถเขียนฟังก์ชั่น Assert ที่อ้างถึงวิธีการบูลีน CONST ด้วยวิธีนี้คุณมั่นใจได้ว่าการยืนยันของคุณไม่มีผลข้างเคียงโดยทำให้มั่นใจว่ามีการใช้วิธีบูลีน const เพื่อทดสอบการยืนยัน

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

overkill ถ้าคุณถามฉัน แต่ถ้าฉันจะเริ่มเห็นระดับหนึ่งของการรักษาเนื่องจากการยืนยันฉันจะระวังสองสิ่ง:

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

2
อึศักดิ์สิทธิ์ที่คุณชอบคำว่า "แน่นอน" และการสืบทอด ฉันนับการใช้ 8 ครั้ง
Casey Patton

ใช่ขอโทษฉันมักจะคิดคำพูดมากเกินไป - คงที่ขอบคุณ
lurscher

2

ฉันเขียนด้วยภาษา C # มากกว่าภาษา C ++ แต่ทั้งสองภาษานั้นไม่ได้อยู่ห่างกันมากนัก ใน. Net ฉันใช้ Asserts สำหรับเงื่อนไขที่ไม่ควรเกิดขึ้น แต่บ่อยครั้งฉันก็โยนข้อยกเว้นเมื่อไม่มีวิธีดำเนินการต่อ ตัวดีบัก VS2010 แสดงข้อมูลที่ดีมากมายให้ฉันในข้อยกเว้นไม่ว่าการปรับรุ่นจะเป็นอย่างไร คุณควรเพิ่มการทดสอบหน่วยถ้าคุณทำได้ บางครั้งการบันทึกก็เป็นสิ่งที่ดีที่จะช่วยแก้จุดบกพร่อง

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

ฉันจะจองคำยืนยันสำหรับสถานการณ์ที่ไม่ควรเกิดขึ้น คุณอาจยืนยันมากเกินไปในตอนแรกเนื่องจากการยืนยันนั้นเร็วกว่าในการเขียน แต่ให้ใส่รหัสอีกครั้งในภายหลัง - เปลี่ยนให้บางส่วนเป็นข้อยกเว้นบางส่วนเป็นการทดสอบ ฯลฯ หากคุณมีระเบียบวินัยเพียงพอที่จะล้างความคิดเห็น TODO ทุกครั้ง แสดงความคิดเห็นถัดจากแต่ละรายการที่คุณวางแผนจะทำใหม่และอย่าลืมทำสิ่งที่ต้องทำในภายหลัง


หากรหัสของคุณไม่ผ่านการยืนยัน 15 ครั้งต่อนาทีฉันคิดว่ามีปัญหาที่ใหญ่กว่านี้ การยืนยันไม่ควรเริ่มด้วยรหัสที่ปราศจากข้อบกพร่องและทำเช่นนั้นพวกเขาควรจะฆ่าแอปพลิเคชันเพื่อป้องกันความเสียหายเพิ่มเติมหรือส่งคุณเข้าสู่ดีบั๊กเพื่อดูว่าเกิดอะไรขึ้น
5gon12eder

2

ฉันต้องการทำงานกับคุณ! คนที่เขียนเยอะมากassertsเป็นเรื่องมหัศจรรย์ ฉันไม่รู้ว่ามีสิ่งใดที่ "มากเกินไป" ห่างไกลกันมากขึ้นกับผมเป็นคนที่เขียนน้อยเกินไปและในที่สุดก็สิ้นสุดการทำงานเป็นปัญหาร้ายแรง UB เป็นครั้งคราวเท่านั้นที่แสดงบนพระจันทร์เต็มดวงที่จะได้รับการทำซ้ำได้อย่างง่ายดายด้วยซ้ำ ๆ assertที่เรียบง่าย

ข้อความล้มเหลว

สิ่งหนึ่งที่ฉันคิดได้ก็คือฝังข้อมูลความล้มเหลวไว้ในassertหากคุณยังไม่ได้ทำเช่นนั้น:

assert(n >= 0 && n < num && "Index is out of bounds.");

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

ผลข้างเคียง

แน่นอนassertสามารถนำไปใช้ในทางที่ผิดและแนะนำข้อผิดพลาดเช่น:

assert(foo() && "Call to foo failed!");

... ถ้าเป็นfoo()ต้นเหตุของผลข้างเคียงดังนั้นคุณควรระวังให้มาก แต่ฉันแน่ใจว่าคุณเป็นคนที่ยืนยันอย่างอิสระอยู่แล้ว หวังว่าขั้นตอนการทดสอบของคุณจะดีพอ ๆ กับความตั้งใจของคุณในการยืนยันสมมติฐาน

แก้ไขข้อบกพร่องความเร็ว

ในขณะที่ความเร็วในการดีบั๊กควรอยู่ที่ด้านล่างของรายการลำดับความสำคัญของเรา แต่ครั้งหนึ่งที่ฉันได้ทำการยืนยันมากใน codebase ก่อนที่การรันการดีบักผ่าน debugger จะช้ากว่าการปล่อย100 ครั้ง

เป็นหลักเพราะฉันมีฟังก์ชั่นเช่นนี้:

vec3f cross_product(const vec3f& lhs, const vec3f& rhs)
{
    return vec3f
    (
        lhs[1] * rhs[2] - lhs[2] * rhs[1],
        lhs[2] * rhs[0] - lhs[0] * rhs[2],
        lhs[0] * rhs[1] - lhs[1] * rhs[0]
    );
}

... ที่ทุก ๆ การเรียกร้องoperator[]จะทำการยืนยันอย่าง จำกัด ฉันลงเอยด้วยการแทนที่บางส่วนของสมรรถนะที่สำคัญด้วยสิ่งที่ไม่ปลอดภัยซึ่งไม่ได้ยืนยันว่าจะเพิ่มความเร็วในการดีบักอย่างมากด้วยค่าใช้จ่ายเพียงเล็กน้อยเพื่อความปลอดภัยในการติดตั้งในรายละเอียดเท่านั้น การผลิตมากอย่างเห็นได้ชัดทำให้เสื่อมเสีย (ทำให้ได้รับประโยชน์ในการได้รับการแก้จุดบกพร่องได้เร็วขึ้นมีค่าเกินค่าใช้จ่ายของการสูญเสียไม่กี่อ้าง แต่เพียงสำหรับการทำงานเช่นการทำงานของผลิตภัณฑ์นี้ข้ามซึ่งถูกนำมาใช้ในที่สุดที่สำคัญเส้นทางวัดไม่ได้สำหรับoperator[]ทั่วไป)

หลักการความรับผิดชอบเดี่ยว

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

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


1
ในทางทฤษฎีอาจมีการยืนยันมากเกินไปแม้ว่าปัญหานั้นจะเห็นได้อย่างรวดเร็วจริง ๆ : ถ้าการยืนยันใช้เวลานานกว่าเนื้อของฟังก์ชัน เป็นที่ยอมรับฉันจำไม่ได้ว่าพบว่าในป่า แต่ปัญหาตรงข้ามเป็นที่แพร่หลายแม้ว่า
Deduplicator

@Dupuplicator อ่าใช่ฉันพบกรณีนั้นในกิจวัตรคณิตศาสตร์เวกเตอร์ที่สำคัญเหล่านั้น แม้ว่ามันจะดูเหมือนดีกว่ามากที่จะทำผิดด้านข้างมากเกินไปน้อยเกินไป!

-1

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

ดังนั้นเมื่อมีคนบอกว่ายืนยันว่าเพียงตรวจสอบว่าโค้ดทำอะไรฟุ่มเฟือยที่ไม่ถูกต้อง การยืนยันนั้นตรวจสอบสิ่งที่พวกเขาคิดว่ารหัสทำและจุดทั้งหมดของการยืนยันคือการตรวจสอบว่าการสันนิษฐานว่าไม่มีข้อผิดพลาดในรหัสนั้นถูกต้อง และยืนยันสามารถทำหน้าที่เป็นเอกสารเช่นกัน ถ้าฉันคิดว่าหลังจากรันลูป i == n แล้วมันไม่ชัดเจน 100% จากโค้ดดังนั้น "assert (i == n)" จะเป็นประโยชน์

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

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

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


-3

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


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