Why does list initialization with lambda causes high cyclomatic complexity?
Asked Answered
C

1

6

Initialization of list with lambdas causes high IL cyclomatic complexity: why, and how remove this complexity? For example following code causes the static constructor of the class (which is actually compiler generated) to be very complex: 1 + the list count.

static List<Predicate<string>> list = new List<Predicate<string>>()
{
    s => s == null,
    s=> s.StartsWith(“R”),
    ... With a lot of predicates like that ….
};

Note: complexity is computed with NDepend

Cholesterol answered 26/10, 2010 at 8:28 Comment(0)
A
2

Why? Because ILCC is defined as the number of different jump/branch destinations. That list you are initializing contains a lot of if/then logic, contained within the lambdas. I presume the language-dependent CC is lower?

High cyclomatic complexity is just a hint that your functions are over-complex, and thus hard to understand and maintain and test. Whether that hint is correct in this case would depend on how you use that list of predicates. But it is just that, a hint. Keeping CC low should not be regarded as a law of nature. If you think the code is maintainable and testable, note the high ILCC in your documentation, explain why it doesn't matter, and move on.

Adila answered 26/10, 2010 at 9:1 Comment(7)
Actually if I decompile the code and if I understand well IL (I'm a dummy about it), the lambdas seem to be generated separatly. The branches look more like an artefact of compiler code generation.Cholesterol
Perhaps you could add a brief extract of the decompiled IL, for us who are too lazy to run to VS. I'm surprised if the lambdas aren't actually generated at the point of static initialization to count towards the static constructor complexity.Adila
L_0008: ldsfld class Predicate1<string> Foo.Class1::CS$<>9__CachedAnonymousMethodDelegate12 L_000d: brtrue.s L_0022 L_000f: ldnull L_0010: ldftn bool Foo.Class1::<FooFunction>b__0(string) L_0016: newobj instance void [mscorlib]Predicate1<string>::.ctor(object, native int) L_001b: stsfld class [mscorlib]Predicate1<string> Foo.Class1::CS$<>9__CachedAnonymousMethodDelegate12 L_0020: br.s L_0022 L_0022: ldsfld class Predicate1<string> Foo.Class1::CS$<>9__CachedAnonymousMethodDelegate12 L_0027: callvirt instance void Generic.List1<class [mscorlib]Predicate1<string>>::Add(!0)Cholesterol
It's hard to tell for certain without fuller docs from NDepend (or their source code!), but it seems fairly obvious that they count the IL contents of anonymous delegates (whether from lambdas or written otherwise) against the complexity of their referencing method for ILCC. Which seems natural: after all, you can hide a lot of conditional clauses and alternate code execution paths inside lambdas. After all, there are plenty of ways of using lambdas to complicate code!Adila
Yes, I understand the base idea, BUT: if I make a lambda more complex, it does not change the ILCC. Actually the ILCC only depends on the lambda count. Which is consistent with what I understand from the generated IL: the branching is here only to check the non-nullity of the lambda, independently from the lambda internals. It seems possible to create complicated code with lambdas with low ILCC!Cholesterol
Interesting to note: I hope NDepend accounts for complexities inside lambdas somewhere... Anyway, the key lesson here is that automatic measurements are just proxies for complexity, and don't directly measure complexity as experienced in testing and maintenance. They're just the best we can do to get a bit of objectivity.Adila
Well, that and that on the one hand if something is inherently complicated and we can get a tool to do it for us, we should, but conversely tools tend not to produce particulary readable code unless designed with that as a goal.Flowerlike

© 2022 - 2024 — McMap. All rights reserved.