Cách thực hành tốt nhất để tiếp tục truyền tin từ bên trong một vòng lặp lồng nhau?


8

Đây là một mẫu đơn giản hóa. Về cơ bản, nó kiểm tra một chuỗi từ danh sách chuỗi. Nếu kiểm tra vượt qua, nó sẽ xóa chuỗi đó ( filterStringOut(i);) và không còn cần thiết để tiếp tục bất kỳ kiểm tra nào khác. Do đó, continueđể chuỗi tiếp theo.

void ParsingTools::filterStrings(QStringList &sl)
{
    /* Filter string list */
    QString s;
    for (int i=0; i<sl.length(); i++) {
        s = sl.at(i);

        // Improper length, remove
        if (s.length() != m_Length) {
            filterStringOut(i);
            continue; // Once removed, can move on to the next string
        }          
        // Lacks a substring, remove
        for (int j=0; j<m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */ 
            }
        }
        // Contains a substring, remove
        for (int j=0; j<m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */ 
            }
        } 
    }
}

Làm thế nào để người ta tiếp tục vòng lặp bên ngoài từ bên trong một vòng lặp lồng nhau?

Dự đoán tốt nhất của tôi là sử dụng gotovà đặt một nhãn ở cuối vòng ngoài. Điều đó thôi thúc tôi đặt câu hỏi này, đưa ra cách cấm kỵ goto.

Trong trò chuyện IRC c ++, có ý kiến ​​cho rằng tôi đặt các forvòng lặp trong các hàm bool, nó trả về giá trị true nếu kiểm tra được thông qua. do đó

if ( containsExclude(s)) continue;
if (!containsInclude(s)) continue;

hoặc đơn giản là tôi tạo một boolean cục bộ, đặt nó thành true break, kiểm tra bool và tiếp tục nếu đúng.

Cho rằng tôi đang sử dụng điều này trong một trình phân tích cú pháp, tôi thực sự cần phải ưu tiên hiệu suất trong ví dụ này. Đây có phải là một tình huống gotovẫn còn hữu ích hay là một trường hợp tôi cần cơ cấu lại mã của mình?



3
C ++ không có nhãn phá vỡ, do đó, thông lệ chính thức và được chấp nhận là mô phỏng chúng thông qua goto, mặc dù tiếng xấu của nó. Đừng sợ tên - khái niệm sợ hãi.
Kilian Foth

2
@Akiva: vậy bạn đã thực sự đo lường sự khác biệt hiệu suất? Và chỉ vì bạn đã nghe "goto" là một cách chấp nhận được để thoát ra khỏi một vòng lặp lồng nhau không có nghĩa là việc thay thế giới thiệu một hàm với một tên rõ ràng và súc tích sẽ không thể đọc được hơn.
Doc Brown

3
@Akiva: điểm chuẩn này khá đơn giản, bạn không cần các công cụ hoặc kỹ năng đặc biệt: thiết lập một chương trình nhỏ gọi hàm này với một số dữ liệu thử nghiệm trong một vòng lặp nhiều lần (có thể vài triệu lần) và đo thời gian chạy với một chiếc đồng hồ bấm giờ. Làm tương tự với mã đã dọn sạch. Tôi cá là sự khác biệt sẽ không đáng kể (tất nhiên, đừng quên sử dụng tối ưu hóa trình biên dịch).
Doc Brown

5
Tại sao bạn lại phát minh lại bánh xe ?
Alexander - Tái lập Monica

Câu trả lời:


16

Đừng lồng nhau: chuyển đổi thành các chức năng thay thế. Và có các hàm đó trả về truenếu chúng thực hiện hành động của chúng và các bước tiếp theo có thể được bỏ qua; falsenếu không thì. Bằng cách đó, bạn hoàn toàn tránh được toàn bộ vấn đề làm thế nào để thoát ra khỏi một cấp độ, tiếp tục trong một cấp độ khác, v.v. khi bạn chỉ xâu chuỗi các cuộc gọi với ||(điều này giả sử C ++ dừng xử lý một biểu thức trên a true; Tôi nghĩ là vậy).

Vì vậy, mã của bạn có thể trông giống như sau (Tôi đã không viết C ++ trong nhiều năm, vì vậy nó có thể chứa lỗi cú pháp, nhưng sẽ cung cấp cho bạn ý tưởng chung):

void ParsingTools::filterStrings(QStringList &sl)
{
    QString s;
    for (int i=0; i<sl.length(); i++) {
        s = sl.at(i);

        removeIfImproperLength(s, i) ||
        removeIfLacksRequiredSubstring(s, i) ||
        removeIfContainsInvalidSubstring(s, i);
    }
}

bool removeIfImproperLength(QString s, int i) {
    if (s.length() != m_Length) 
    {
        filterStringOut(i);
        return true;
    }
    return false;
}          

bool removeIfLacksSubstring(QString s, int i) {
    for (int j=0; j<m_Include.length(); j++) {
        if (!s.contains(m_Include.at(j))) { 
            filterStringOut(i);
            return true; 
        }
    }

    return false;
}

bool removeIfContainsInvalidSubstring(QString s, int i) {
    for (int j=0; j<m_Exclude.length(); j++) {
        if (s.contains(m_Exclude.at(j))) { 
            filterStringOut(i); 
            return true;
        }
    } 

    return false;
}

1
"reove IfImproperLpm" Typo. :)
Neil

5
Sẽ tốt hơn nếu ba hàm kiểm tra điều kiện vẫn không có hiệu ứng phụ (nghĩa là thay vì thực hiện "remove-if", chỉ cần trả về điều kiện boolean và để người gọi ( ParsingTools::filterStrings) gọi filterStringOut(i)hàm, như thể hiện trong câu trả lời của dagnelies.
rwong

Vì vậy, bạn đang sử dụng ngữ nghĩa gọi hàm làm cơ sở cho các câu lệnh ngắt bị thiếu của C ++. Rất thông minh.
Ryan Reich

13

Từ góc nhìn chim hơn, tôi sẽ cấu trúc lại mã để nó trông như thế này ... (trong mã giả, cách đây quá lâu tôi đã chạm vào C ++)

void filterStrings(sl)
{
    /* Filter string list */
    for (int i=0; i<sl.length(); i++) {
        QString s = sl.at(i);
        if(!isProperString(s)) {
           filterStringOut(i);
        }
     }
}

bool isProperString(s) {

        if (s.length() != m_Length)
            return false; // Improper length

        for (int j=0; j<m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) { 
                return false; // Lacks a substring
            }
        }

        for (int j=0; j<m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) { 
                return false; // Contains a substring
            }
        }

        return true; // all tests passed, it's a proper string
}

Đây là trình dọn dẹp IMHO vì nó phân tách rõ ràng những gì tạo thành một chuỗi thích hợp và những gì bạn làm khi không.

Bạn thậm chí có thể tiến thêm một bước và sử dụng các phương thức lọc tích hợp như myProperStrings = allMyStrings.filter(isProperString)


10

Tôi thực sự thích cách @dagnelies bắt đầu . Ngắn và đến điểm. Một sử dụng tốt của trừu tượng cấp cao. Tôi chỉ điều chỉnh chữ ký của nó và tránh tiêu cực không cần thiết.

void ParsingTools::filterStrings(QStringList &sl)
{
    for (int i=0; i<sl.length(); i++) {
        QString s = sl.at(i);
        if ( isRejectString(s) ) {
            filterStringOut(i);
        }
    }
}

Tuy nhiên, tôi thích cách @DavidArno phá vỡ các bài kiểm tra yêu cầu dưới dạng các hàm riêng lẻ. Chắc chắn toàn bộ mọi thứ trở nên dài hơn nhưng mọi chức năng đều nhỏ bé một cách tuyệt vời. Tên của họ tránh sự cần thiết phải bình luận để giải thích những gì họ đang có. Tôi chỉ không thích rằng họ đảm nhận thêm trách nhiệm gọi điện filterStringOut().

Nhân tiện, có C ++ sẽ ngừng đánh giá ||chuỗi trong một truethời gian miễn là bạn không làm quá tải ||toán tử. Điều này được gọi là đánh giá ngắn mạch . Nhưng đây là một tối ưu hóa vi mô tầm thường mà bạn có thể bỏ qua khi bạn đọc mã miễn là các chức năng không có tác dụng phụ (chẳng hạn như các chức năng dưới đây).

Sau đây sẽ làm rõ định nghĩa của chuỗi từ chối mà không kéo bạn qua các chi tiết không cần thiết:

bool isRejectString(QString s) {
    return isDifferentLength(s, m_Length) 
        || sansRequiredSubstring(s, m_Include)
        || hasForbiddenSubstring(s, m_Exclude)
    ;
}

Giảm nhu cầu gọi filterStringOut()các hàm kiểm tra yêu cầu trở nên ngắn hơn và tên của chúng đơn giản hơn nhiều. Tôi cũng đã đặt mọi thứ họ phụ thuộc vào danh sách tham số của họ để giúp họ dễ hiểu hơn mà không cần nhìn vào bên trong.

bool isDifferentLength(QString s, int length) {
    return ( s.length() != length );
}

bool sansRequiredSubstring(QString s, QStringList &include) {
    for (int j=0; j<include.length(); j++) {
        QString requiredSubstring = include.at(j);
        if ( !s.contains(requiredSubstring) ) { 
            return true; 
        }
    }
    return false;
}

bool hasForbiddenSubstring(QString s, QStringList &exclude) {
    for (int j=0; j<exclude.length(); j++) {
    QString forbiddenSubstring = exclude.at(j);
        if ( s.contains(forbiddenSubstring) ) { 
            return true; 
        }
    }
    return false;
}

Tôi đã thêm requiredSubstringforbiddenSubstringcho con người. Họ sẽ làm bạn chậm lại? Kiểm tra và tìm hiểu. Làm cho mã dễ đọc thực sự nhanh hơn sau đó làm cho mã được tối ưu hóa sớm có thể đọc được hoặc thực sự nhanh.

Nếu các chức năng hóa ra làm chậm bạn, hãy xem xét các chức năng nội tuyến trước khi bạn đặt con người vào mã không thể đọc được. Một lần nữa, đừng cho rằng điều này sẽ cung cấp cho bạn tốc độ. Kiểm tra.

Tôi nghĩ bạn sẽ tìm thấy bất kỳ thứ nào trong số này dễ đọc hơn các vòng lặp lồng nhau. Những người, kết hợp với if, đã bắt đầu cung cấp cho bạn một mẫu chống mũi tên thực sự . Tôi nghĩ rằng bài học ở đây là các chức năng nhỏ là một điều tốt.


1
Mặc dù đó là sự kết hợp của hai câu trả lời khác, nhưng điều này làm tăng thêm nhiều giá trị. Làm cho nó thành tiếng vang cho con người, kiểm tra hiệu năng trước khi bạn quyết định làm lu mờ mã Code và làm cho nó dễ kiểm tra. Công cụ tuyệt vời!
carlossierra

1
Trên thực tế, việc tôi sử dụng ! isProperStringchứ không phải isImproperStringlà cố ý. Tôi có xu hướng tránh tiêu cực trong tên chức năng. Hãy tưởng tượng bạn cần kiểm tra xem đây có thực sự là một chuỗi thích hợp hay không, bạn sẽ cần !isImproperStringIMHO dễ bị nhầm lẫn hơn vì phủ định kép.
dagnelies

@dagnelies tốt hơn?
candied_orange

4

Chỉ cần sử dụng lambda cho vị ngữ và sau đó sử dụng sức mạnh của các thuật toán tiêu chuẩn và ngắn mạch. Không cần bất kỳ dòng điều khiển phức tạp hoặc kỳ lạ:

void ParsingTools::filterStrings (QStringList& list)
{
    for (int i = list.size(); i--;) {
        const auto& s = list[i];
        auto contains = [&](const QString& x) { return s.contains(x); };
        if (s.size() != m_Length
                || !std::all_of(m_Include.begin(), m_Include.end(), contains)
                || std::any_of(m_Exclude.begin(), m_Exclude.end(), contains))
            filterStringOut(i);
    }
}

1

Ngoài ra còn có tùy chọn để làm cho nội dung của vòng lặp bên ngoài (cái bạn muốn tiếp tục) thành lambda , và chỉ cần sử dụng return.
Thật dễ dàng đáng ngạc nhiên nếu bạn biết lambdas; về cơ bản bạn bắt đầu nội thất vòng lặp của bạn với [&]{và kết thúc nó với }(); bên trong bạn có thể sử dụng return;bất cứ lúc nào để rời khỏi nó:

void ParsingTools::filterStrings(QStringList &sl)
{
    /* Filter string list */
    QString s;
    for (int i=0; i<sl.length(); i++) {

      [&]{    // start a lamdba defintion

        s = sl.at(i);

        // Improper length, remove
        if (s.length() != m_Length) {
            filterStringOut(i);
            // continue; // Once removed, can move on to the next string
            return; // happily return here, this will continue 
        }          
        // Lacks a substring, remove
        for (int j=0; j<m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */  return;  // happily return here, this will continue the i-loop
            }
        }
        // Contains a substring, remove
        for (int j=0; j<m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */  return; // happily return here, this will continue the i-loop
            }
        } 

      }()   // close/end the lambda definition and call it

    }
}

3
(1) Để thực sự gọi lambda ngay lập tức , ở phần cuối của dấu ngoặc nhọn đóng, người ta phải thực hiện cuộc gọi (với một cặp dấu ngoặc đơn, có khả năng có danh sách các đối số hoặc không có). (2) cả ba nơi sử dụng continuebreakphải được thay thế bằng return. Mã của bạn dường như giữ nguyên vị trí đầu tiên (sử dụng continue), nhưng điều đó cũng phải được thay đổi, bởi vì mã nằm trong lambda và continuecâu lệnh không thể tìm thấy một phạm vi là một vòng lặp.
rwong

Tôi đã viết điều đó trong khi chờ đèn đỏ. Đã sửa.
Aganju

1

Tôi nghĩ rằng @dganelies có ý tưởng đúng như một điểm khởi đầu, nhưng tôi nghĩ tôi sẽ xem xét tiến thêm một bước: viết một hàm chung có thể thực hiện cùng một mô hình cho (gần như) bất kỳ container, tiêu chí và hành động nào:

template <class Container, class Action, class Condition>
void map_if(Container &container, Action action, Condition cond) {
    for (std::size_t i = 0; i < container.length(); i++) {
        auto s = container.at(i);
        if (cond(s))
            action(i);
    }
}

Của bạn filterStringssau đó sẽ chỉ xác định các tiêu chí, và vượt qua các hành động thích hợp:

void ParsingTools::filterStrings(QStringList const &sl)
{
    auto isBad = [&](QString const &s) {

        if (s.length() != m_Length)
            return true;

        for (int j = 0; j < m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) {
                return true;
            }
        }

        for (int j = 0; j < m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) {
                return true;
            }
        }
        return false;
    };

    map_if(sl, filterStringOut, isBad);
}

Tất nhiên, có nhiều cách khác để tiếp cận vấn đề cơ bản đó. Ví dụ: sử dụng thư viện chuẩn, bạn dường như muốn một cái gì đó theo cùng một thứ tự chung như std::remove_if.


1

Một số câu trả lời cho thấy một bộ tái cấu trúc chính của mã. Đây có lẽ không phải là một cách tồi để đi, nhưng tôi muốn cung cấp một câu trả lời phù hợp hơn với chính câu hỏi.

Quy tắc số 1: Hồ sơ trước khi tối ưu hóa

Luôn luôn hồ sơ kết quả trước khi cố gắng tối ưu hóa. Bạn có thể thấy mình lãng phí rất nhiều thời gian nếu không.

Điều đó đang được nói ...

Vì vậy, cá nhân tôi đã thử nghiệm loại mã này trên MSVC. Booleans là con đường để đi. Đặt tên cho boolean một cái gì đó có ý nghĩa về mặt ngữ nghĩa containsString.

    ...
    boo containsString = true; // true until proven false
    // Lacks a substring, remove
    for (int j=0; j<m_Include.length(); j++) {
        if (!s.contains(m_Include.at(j))) { 
            filterStringOut(i); 
            /* break; and continue; */ 
            containsString = false;
        }
    }
    if (!containsString)
        continue;

Trên MSVC (2008), ở chế độ phát hành (cài đặt tối ưu hóa điển hình), trình biên dịch đã tối ưu hóa một vòng lặp tương tự xuống chính xác cùng một bộ opcode như gotophiên bản. Nó đủ thông minh để thấy rằng giá trị của boolean được gắn trực tiếp để kiểm soát dòng chảy, và tránh xa mọi thứ. Tôi chưa thử nghiệm gcc, nhưng tôi cho rằng nó có thể thực hiện các loại tối ưu hóa tương tự.

Điều này có lợi thế hơn gotolà chỉ đơn giản là không gây ra bất kỳ mối quan tâm nào bởi những người theo chủ nghĩa thuần túy, những người coi gotolà có hại, mà không phải hy sinh giá trị hiệu suất của một chỉ dẫn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.