코드 가독성

아래 메서드 네개는 문자열 시퀀스를 정수 시퀀스로 파싱하고, 파싱된 정수 시퀀스 중에 가장 큰 두 개의 합을 구하고 있습니다. (결과는 모두 동일합니다)

이들을 가독성이 높은 순으로 나열해주세요. ^^
일종의 앙케이트 같은 것이니, 편안하게 본인의 의견을 말씀해주시면 좋겠습니다.

(소스 코드 출처는 선입견을 배제하기 위해, 나중에 밝히도록 하겠습니다)

int Method1(IEnumerable<string> items)
{
    var all = new List<int>();
    using var enumerator = items.GetEnumerator();

    while(enumerator.MoveNext())
    {
        if(int.TryParse(enumerator.Current, out var number)) all.Add(number);
    }

    if(all.Count < 2) return -1;

    int[] topTwo = { all[0], all[1] };
    
    if( all[1] > all[0] )
    {
        topTwo[0] = all[1];
        topTwo[1] = all[0];
    } 

    for (int i = 2; i < all.Count; i++)
    {
        if(all[i] > topTwo[0])
        {
            topTwo[1] = topTwo[0];
            topTwo[0] = all[i];
        } 
        else if(all[i] > topTwo[1]){
            topTwo[1] = all[i];
        }
    }

    return topTwo[0] +  topTwo[1];
}
int Method2(IEnumerable<string> items)
{
    var all = new List<int>();
    foreach (var item in items)
    {
        if(int.TryParse(item, out var num)) all.Add(num);
    }

    if(all.Count < 2) return -1;

    var (a, b) = all[0] > all[1] ? ( all[0], all[1] )
        : ( all[1], all[0] );

    foreach (var num in all.Skip(2))
    {
        if (num > a) (a, b) = (num, a);
        else if(num > b) b = num;
    }

    return a + b;
}
int Method3(IEnumerable<string> items)
{
    var (a, b, count) = (0, 0, 0);
    foreach (var item in items)
    {
        if(!int.TryParse(item, out var num)) continue;

        (a, b, count) = count switch
        {
            0 => (num, 0, 1),
            1 when num > a => (num, a, 2),
            1 => (a, num, 2),
            2 when num > a => (num, a, 2),
            2 when num > b => (a, num, 2),
            _ => (a, b, count),
        };
    }
    return count < 2 ? -1 : a + b;
}
int Method4(IEnumerable<string> items) => items.Aggregate
(
    ( max: 0, next: 0, count: 0), 
    (a, item) => int.TryParse(item, out var num) ? a switch
        {
            (_, _, 0) => (num, 0, 1),
            (var max, _, 1) when num > max => (num, max, 2),
            (var max, _, 1) => (max, num, 2),
            (var max, _, 2) when num > max => (num, max, 2),
            (var max, var next, 2) when num > next => (max, num, 2),
            _ => a,
        } 
        : a, 
    (a) => a.count < 2 ? -1 : a.max + a.next 
);
1 Like

Method3 - 2 - 4 - 1

1 Like

4

1 Like

동일 기능을 하는 코드를 짜봤습니다.

string[] items = ["5", "2", "4", "1"];

Console.WriteLine(Method(items));


static int Method(IEnumerable<string> items)
{
    var result = items
        .Where(x => int.TryParse(x, out _) is true)
        .Select(x => int.Parse(x))
        .OrderDescending()
        .Take(2);
    if (result.Count() < 2)
        return -1;

    return result.Sum();
}
3 Likes

출처에서도 비슷한 댓글이 있었습니다.

... => items.Select( x => int.TryParse(x,out int num) ? num : 0)
            .OrderByDescending(i=>i).Take(2).Sum();

위 식에 대해 출처의 저자는 아래와 같은 특징이 있다고 했습니다.

  • 시간 복잡도 : O(n logn)
  • 공간 복잡도 : O(n)

위 식은 items 의 갯수가 작을 때, 다시 말하면, 시퀀스의 크기 한도가 미리 주어진 경우에 사용할 수 있다고 합니다.

출처의 경우, 그런 조건이 제시되지 않았으므로, 제시된 메서드들은 공통적으로 아래의 특징을 보입니다.

  • 시간 복잡도 : O(n)
  • 공간 복잡도 : O(n) or O(1)
2 Likes

정답이 존재하면 정답으로 작성하면 되는데
항상 성능과 트래이드 오프 관계라서

리펙토링 저자 마틴 파울러의 의견은

1 Like

C# 사용자로서, 각 코드의 가독성 정도를 어떻게 느끼는 지 물어 보는 것이라, 정답은 있을 수 없죠.

참고로, 다른 댓글에서 언급한 것처럼, 제시된 메서드들의 성능은 대동소이합니다.
제 생각엔 저자가 다른 논쟁을 피하기 위해, 성능이 다른 표현 방식을 일부러 제외한 듯 합니다.

그리고, 구현 알고리즘도 유사합니다.
오로지, 언어에서 취사 선택 가능한 표현 방식의 차이만 존재합니다.

이런 식도 가능할 것 같습니다.

static int MethodD2(IEnumerable<string> items)
{   
    var max = (-1, -1);
    foreach (var item in items)
    {
        if (int.TryParse(item, out var intItem) is false)
            continue;

        if (intItem > max.Item1)
            max = (intItem, max.Item1);
        else if (intItem > max.Item2)
            max = (max.Item1, intItem);
    }
    if (max.Item1 is -1 || max.Item2 is -1)
        return -1;

    return max.Item1 + max.Item2;
}
2 Likes

예, 그렇게도 가능하고, 이전에 보여주신 코드도 가능합니다.

다만, 제시된 코드들의 가독성을 개인 별로 어떻게 달리 느끼는 지 알고 싶었습니다.

1 Like