8
\$\begingroup\$

One fun feature that was added (fairly) recently in .NET is the [CallerLineNumber] attribute, which, in conjunction with [CallerFilePath] and [CallerMemberName] can allow for some very nice logging methods.

I was thinking today, though, that they might be used for another interesting purpose. Consider the case where you have some event that should trigger on a Boolean transition. The classic implementation for this uses a field to keep track of the 'last state', then updates the field after performing the check. The annoying thing, though, is that you need a flag for every case and each flag needs a name. So I thought of something like this:

public sealed class IndexedBuilder<T>
    where T : class, new()
{
    private readonly Dictionary<int, T> _instances = new Dictionary<int, T>();

    public T Build(int index)
    {
        T instance;

        lock (_instances)
        {
            if (!_instances.TryGetValue(index, out instance))
            {
                instance = new T();
                _instances.Add(index, instance);
            }
        }

        return instance;
    }
}

public sealed class TransitionDetector
{
    private readonly IndexedBuilder<Context> _builder = new IndexedBuilder<Context>();

    public bool DetectUp(bool state, [CallerLineNumber] int lineNumber = 0)
    {
        return _builder.Build(lineNumber).DetectUp(state);
    }

    public bool DetectDown(bool state, [CallerLineNumber] int lineNumber = 0)
    {
        return _builder.Build(lineNumber).DetectDown(state);
    }

    private sealed class Context
    {
        private bool? _lastState;

        public bool DetectUp(bool state)
        {
            bool result = state && (_lastState == false);
            _lastState = state;
            return result;
        }

        public bool DetectDown(bool state)
        {
            bool result = !state && (_lastState == true);
            _lastState = state;
            return result;
        }
    }
}

public static class Program
{
    public static void Main()
    {
        var detector = new TransitionDetector();

        int x = 0;
        int y = 0;
        while (x < 10000)
        {
            if (detector.DetectUp(y > 100))
                Console.WriteLine("y > 100");

            if (detector.DetectDown(x < 500))
                Console.WriteLine("x >= 500");

            y++;
            x++;

            if (y > 1000)
                y = 0;
        }

        Console.ReadLine();
    }
}

This is a specific example of the more general pattern of allowing an instance to be bound to a particular location in the code, without explicitly naming or referencing the instance. The biggest issue I see is that this breaks down if you attempt to perform multiple calls on the same line in the code or if you pass the TransitionDetector instance such that it gets called from another file, which means documentation required and there is a significant potential for incorrect use.

\$\endgroup\$
  • \$\begingroup\$ I'm going to answer this question, but first I need to sit down and take some time with the code and think it through all the way to really be able to answer your question. My first thoughts are that it may not in fact be a clever hack or all bad, that there could be some interesting applications with this in F#, or functional C#. It could also be used the same way as a preprocessor directive, although since the hook is actually invoked in code, you get this weird meta-solution. \$\endgroup\$ – moarboilerplate Apr 16 '15 at 19:08
  • \$\begingroup\$ @moarboilerplate, one extension I've thought of is using [CallerFilePath] in conjunction with the line number to create a string key that allows it to be used across multiple files. The cases I've thought of so far, though, are always local to a particular class; the main use case I've been thinking of is for some profiling functionality (keeping track of robot motion times for particular moves, then using those to improve an internal estimate of the particular motion's duration for scheduling purposes.) This kind of local scoping is a lot easier than an explicit instance for each move. \$\endgroup\$ – Dan Bryant Apr 16 '15 at 20:33
3
\$\begingroup\$

This is definitely an interesting use of [CallerLineNumber] but I think that the risk for people using it wrong resulting in hard to find bugs is to great. If you take an example from the issue you yourself mentioned.

if (detector.DetectUp(y > 100) ||
    detector.DetectUp(x < 500))
{...}

Would not as one might think have the same behavior as

if (detector.DetectUp(y > 100) || detector.DetectUp(x < 500))
{...}

Another gotcha I found was that lambda functions does not give the line or name of where they are called but where they are defined. This makes sense but might not be what the user expects. The example below prints Main: 10 and not HelpFun: 16.

using System;
using System.Runtime.CompilerServices;

namespace CallerLineNumberTest
{
    class Program
    {
        static void Main(string[] args)
        {
            HelpFun(() => PrintLineNr());
            Console.ReadKey();
        }

        private static void HelpFun(Action a)
        {
            a();
        }

        private static void PrintLineNr([CallerMemberName] string name = "",
            [CallerLineNumber] int lineNumber = 0)
        {
            Console.WriteLine(name + ": " + lineNumber);
        }
    }
}
|improve this answer|||||
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for?Browse other questions tagged or ask your own question.