Sunday, May 4, 2014

Рецензия на книгу "More Effective C#: 50 Specific Ways to Improve Your C#"

Здравствуйте, уважаемы читатели моего блога. В этой статье поделюсь размышлениями о книге Билла Вагнера  "More Effective C#: 50 Specific Ways to Improve Your C#". Я уже писал рецензию на первую книгу Вагнера "Effective C#", так что продолжу традицию тезисным отзывом о второй книге этого автора. Если посмотрите статью-рецензию на первую книгу Вагнера по указанной выше ссылке, вы сможете найти ссылку на отзыв известного в Украине MVP Сергея Теплякова, с блогом которого можно ознакомиться по ссылке. Я полностью согласен с Сергеем по поводу первой части книги: стиля изложения, а также информации, которая является актуальной, но, к сожалению, характеризуется наличием неточностей в терминах, имеющих непосредственное отношение к языку C#  (тот же WPF и Windows Forms), а также ошибок в тексте, что заслуживает для этой книги оценки "3". Книга будет полезна разработчикам для понимания нюансов языка C#, с которыми вы, возможно, не сталкивались, но особой надежды на эту книгу не стоит возлагать. Она слишком дорога, по сравнению с такими бестселлерами, как  "CLR via C# (4th Edition) (Developer Reference)" Рихтера и книга Джона Скита "C# in Depth, 3rd Edition". Кстати, Скит написал обзор книги Вагнера в своем блоге "Book Review: Effective C# (2nd edition) by Bill Wagner"
Я не согласен с некоторыми трактовками Скита, так как он пишет в своем стиле, чтобы не обидеть своего собеседника, даже если неточность в книге Билла бросается Скиту в глаза не один раз. Скит является по истине гениальным разработчиком и автором книг по языку C#, и если вы о нем никогда не слышали, пожалуй, появляются некоторые сомнения в вашем мастерстве разработчика C#. Сложно представить, что профессиональный разработчик на языке C#  ни разу не пользовался сайтом stackoverflow.com. Внешний вид книги выглядит так:
Сам Вагнер написал, что переиздание Effective C# по некоторым аспектам у него раскрыто более полно, чем издание, на которое я решился написать рецензию.
Comment from Bill: generics and lambda expressions (and LINQ) are covered in some detail in More Effective C#. It's a bit strange that as of the 2nd edition, Effective C# covers a newer version of the language than More Effective C#. I tried hard to make sure neither book expects a reader to have read the other, but the organization of both books as a whole does show the hazards of hitting a moving target.
Цитата взята с блога Скита, где можно посмотреть комментарии в тексте статьи и к статье. 
Так как я до сих пор читаю эту книгу, то постараюсь раскрывать проблемы по мере их обнаружения в книге. Как показывает практика, это более эффективно, чем позже вспоминать о том, в каком совете ты увидел проблему. Поэтому рецензия будет опубликована с прочтением всех глав. 
Книга дает ценные советы по языку C# 3.0 и о том, как нужно использовать те или иные аспекты этого языка. Напомню некоторым читателям, что C# 3.0 вышел с появлением .NET 3.5, в котором уже появились лямбда-выражения, extension методы, использования для типа var, деревья выражений и многое другое. Это для того, чтобы начинающие разработчики знали, что было сделано нового в языке C# 3.0 и что пытался нам донести Вагнер.
Начну с позитивных моментов. Некоторые темы в книге раскрыты очень подробно и акцентируют внимание разработчика на основных моментах. Примером является описание работы с дженериками в первых пяти главах книги, где затрагиваются ограничения на generic, использование интерфейсов IEqualityComparer и IComparable, работа с делегатами и многое другое. Все описано на профессиональном  и доступном для разработчика уровне.
Но вот совет №6 вызвал у меня небольшое недоумение: "Use Delegates to Define Method Constraints on Type Parameters". И приведен пример, как с помощью делегата можно определить выходной параметр. Ниже приведен пример, как распарсить с входного потока через TextReader.
public delegate T CreateFromStream<T>(TextReader reader);
public class InputCollection<T>
{
       private List<T> thingsRead = new List<T>();
       private readonly CreateFromStream<T> _readFunc;

       public InputCollection(CreateFromStream<T> readFunc)
       {
             _readFunc = readFunc;
       }

       public void ReadFromStream(TextReader reader)
       {
             thingsRead.Add(_readFunc(reader));
       }

       public IEnumerable<T> Values
       {
             get { return thingsRead; }
       }
}

public static Point ParsePoint(TextReader reader)
{
       string line = reader.ReadLine();
       var point = new Point();
       string[] fields = line.Split(',');
       if (fields.Length != 2)
             throw new InvalidOperationException(
                    "Input format incorrect");
       double value;
       if (!Double.TryParse(fields[0], out value))
             throw new InvalidOperationException(
                    "Could not parse X value");
       point.X = value;
       if (!Double.TryParse(fields[1], out value))
             throw new InvalidOperationException(
                    "Could not parse Y value");
       point.Y = value;

       return point;
}
public static int? Result(int? a, int? b)
{
       return a + b;
}
Я немного изменил исходный код, так как он был реально нерабочий. Точнее он будет читать только одно значение из файла, либо нужно писать логику обработки над функцией ReadFromStream сверху. Чтобы такой подход заработал, данный код нужно переписать так:
public class Point
{
       public int X { get; set; }
       public int Y { get; set; }
}

public delegate T CreateFromStream<T>(TextReader reader);
public class InputCollection<T>
{
       private List<T> thingsRead = new List<T>();
       private readonly CreateFromStream<T> _readFunc;

       public InputCollection(CreateFromStream<T> readFunc)
       {
             _readFunc = readFunc;
       }

       public void ReadFromStream(TextReader reader)
       {
             while (reader.Peek() >= 0)
             {
                    thingsRead.Add(_readFunc(reader));
             }
       }

       public IEnumerable<T> Values
       {
             get { return thingsRead; }
       }
}
Код, который это проделывает, выглядит следующим образом:
public static Point ParsePoint(TextReader reader)
{
       string line = reader.ReadLine();
       var point = new Point();
       string[] fields = line.Split(',');
       if (fields.Length != 2)
             throw new InvalidOperationException(
                    "Input format incorrect");
       int value;
       if (!Int32.TryParse(fields[0], out value))
             throw new InvalidOperationException(
                    "Could not parse X value");
       point.X = value;
       if (!Int32.TryParse(fields[1], out value))
             throw new InvalidOperationException(
                    "Could not parse Y value");
       point.Y = value;

       return point;
}
Тестовые данные:
var stream = new MemoryStream();

var sb = new StringBuilder();
sb.AppendFormat("{0},{1}{2}", 127, 134, Environment.NewLine);
sb.AppendFormat("{0},{1}{2}", 432, 54, Environment.NewLine);
sb.AppendFormat("{0},{1}{2}", 23, 45, Environment.NewLine);
sb.AppendFormat("{0},{1}{2}", 43, 134, Environment.NewLine);

var body = Encoding.UTF8.GetBytes(sb.ToString());
stream.Write(body, 0, body.Length);
stream.Position = 0;
using (TextReader reader = new StreamReader(stream))
{
       InputCollection<Point> readValues = new
             InputCollection<Point>(
             ParsePoint);
       readValues.ReadFromStream(reader);
       foreach (var point in readValues.Values)
       {
             Console.WriteLine("X = {0}, Y = {1}", point.X, point.Y);
       }
}
В примере я изменил класс Point и поля класса Point с double на int. В примера автора с парсингом есть проблема с кодировкой. В американской локали double значения представлены через точку, например, число 127.6. В русской локали разделитель обычно используется запятая. Поэтому ваш пример не заработает. Возможно, для кого-то это мелочь, так как главное − донести цель, но для меня такие ошибки сразу бросаться в глаза.
Также некоторое смятение вызвал совет "Item 9: Prefer Generic Tuples to Output and Ref Parameters". Сам совет немного не вяжется с примерами, которые демонстрирует автор. Первым делом приводится пример с использованием методов TryParse. Если вы помните, то эти методы возвращают результат успешного парсинга в out параметре, а сама функция возвращает булевое значение, которое означает успех операции или ошибку. В таком примере мы не можем использовать Tuple, так как это исключение из правил. Давайте представим себе псевдо-пример, в котором будем возвращать для такой функции как TryParse первым результатом наш результат конвертации в нужный тип.
public static Tuple<bool, int> TryParse(string a)
{
       try
       {
             return new Tuple<bool, int>(true, Int32.Parse(a));
       }
       catch (Exception)
       {
             return new Tuple<bool, int>(false, 0);
       }
                   
}
Использование этой функции:
var result = TryParse("1");
if (result.Item1)
{
       Console.WriteLine("Success");
}
Возникает вопрос: разве использование Item1,Item2…ItemN улучшило читабельность кода? Теперь попробуем усложнить пример, используя Tuple для возврата нескольких значений, как делает автор для примера.
public static Tuple<string, decimal> FindTempForNearestCity
       (string soughtCity)
{
       string city = "algorithmElided";
       decimal temp = decimal.MinValue; // really cold.
       return new Tuple<string, decimal>(city, temp);
}
Обратите внимание на то, что Вагнер считает такой подход не очень хорошим не из-за использования Tuple, а из-за того, что лучше читалось бы так:
using CityTemperature = System.Tuple<string, decimal>;
public static CityTemperature FindTempForNearestCity
       (string soughtCity)
{
       string city = "algorithmElided";
       decimal temp = decimal.MinValue; // really cold.
       return new CityTemperature(city, temp);
}
Для меня такой подход немного лучше, чем использование того же dynamic. Подход не улучшает качество кода, а говорит обычно о том, что автору в большинстве случаев просто лень создать нормальную модель данных. Поэтому код с использованием Tuple вместо ref/out параметра говорит о непродуманности архитектуры. В своей статье "6 вещей, которые (не)обязан делать C# разработчик", в которой я рассматривал статью Pawel Bejger, project manager компании Goyello. Я там уже упоминал по поводу ref и out. Приведу процитированную часть, на которую хотел бы обратить внимание сейчас.
Try to avoid using the “ref” and “out” keywords
В данном описании соглашусь с автором, что данный подход нарушает Single Responsibility Principle (Принцип Единой Ответственности), но у нас есть обратная сторона медали, которую автор называет "исключением из правил". Примером такого исключения служит изменение value type при передаче в метод функции и т.д. (часто такой подход наблюдается при использовании Win32 API структур данных). Использование для передачи без параметра ref приводит к копированию значения. Но в целом автор прав. Если в Вашем коде часто используется ref или out, то с большой вероятностью здесь проблема в реализации, и классы пытаются выполнить код, который они не должны делать.
Так что ищите проблему в другом месте, так как в 90% случаев у вас проблема в проектировании ваших классов и функций, поэтому использование Tuple не решит проблему вообще.
Следующий раздел Multithreading in C#, который рассказывает о работе с многопоточностью, немного о побочных эффектах и таком явлении, как deadlock. Но из приведенного примера понять, что такое deadlock, сложно, так как пример не полный. Из-за попытки компактности в итоге немного пострадало объяснение примера. Но в целом для введения в многопоточность пример имеет место быть.
Пример "Item 11: Use the Thread Pool Instead of Creating Threads" должны использовать все разработчикиЕсли вы все еще создаете много разных потоков явно, посмотрите в сторону ThreadPool и сравните разницу. Та же самая библиотека TPL работает, по сути, внутри через ThreadPool.
Совет "Item 12: Use BackgroundWorker for Cross-Thread Communication" больше подходит для работы с UI в Windows Forms. Также использование BackgroundWorker с выходом TPL стало не таким популярным, но очень часто встречается в Windows Forms для обновления UI кода и для отображения прогресса выполнения задачи. Особенно часто из-за ProgressBar его и используют. Также этот компонент прост в обращении и использовании. В целом вы можете найти много ценных советов по работе с многопоточностью.
Единственное, что никак не вписывается в данную книгу, − так это совет "Item 16: Understand Cross-Thread Calls in Windows Forms and WPF". Согласен с автором по поводу данного совета, но не понимаю, как тема WPF и Windows Forms пересекается с языком C# как таковым. С таким же успехом можно затронуть тему кеширования в Web Forms, или работу с ADO.NET, или WCF? Эта тема немного не из той "степи". Я предполагал, что ошибку с использованием WPF в книге Effective C# автор допустил не специально, но, наверное, я ошибался. К тому же, в примере автора была ошибка с написанием extension метода для Control.Invoke. Этот метод можно переписать так:
public static class ControlExtensions
{
    public static void Invoke(Control invokeControl,
                                Delegate method,
                                params object[] args)
    {
        if (invokeControl == null)
            throw new ArgumentNullException("invokeControl");

        if (method == null)
            throw new ArgumentNullException("method");

        invokeControl.InvokeIfRequired(method, args);
    }

    public static void Invoke(this Control invokeControl,
                                Action action)
    {
        if (invokeControl == null)
            throw new ArgumentNullException("invokeControl");

        invokeControl.InvokeIfRequired(action);
    }

    private static void InvokeIfRequired(this Control invokeControl,
                                            Delegate method,
                                            params object[] args)
    {
        if (IsControlNotAccesible(invokeControl))
            return;

        if (invokeControl.InvokeRequired)
        {
            try
            {
                invokeControl.Invoke(method, args);
            }
            catch (ObjectDisposedException) { }
        }
        else
        {
            method.Method.Invoke(method.Target, args);
        }
    }

    private static bool IsControlNotAccesible(Control control)
    {
        return control == null ||
                control.IsDisposed ||
                control.Disposing;
    }


}
Необходимо добавить проверку, доступен ли контрол, а также ловить в catch ошибку ObjectDisposedException, которая говорит о том, что контрол уже недоступен.
Следующий совет "Item 17: Create Composable APIs for Sequences" стоит просто дополнить, так как автор рассказывает в правильном направлении. Лучше такой подход, как Composable APIs, для последовательностей делать как extension методы, тогда у нас упрощается вызов кода, и мы не теряем исходный функционал. Ко всему прочему, extensions методы выносятся в отдельные статические классы, и за кодом намного проще следить, так как он лежит в одном месте.
Один из советов, в котором я согласен с автором только на 50% процентов, есть совет "Item 22: Prefer Defining Methods to Overloading Operators". Не согласен с этим советом, потому что в некоторых случаях перегрузка оператора позволяет упростить понимание кода. В пример приведу статью ExpandoObject to Xml, которая основана на ElasticObject (класс, наследуемый от ExpandoObject, работа с DLR) и показывает пример того, как с помощью операторов можно улучшить понимание кода.
Считаю очень полезным совет "Item 25: Use Exceptions to Report Method Contract Failures", в котором автор предлагает использовать функцию для проверки аргументов, которая будет возвращать булевый результат: валидные данные попадают в функцию или нет. В современном подходе к разработке на языке C# это, по сути, основа контрактов, когда делается проверка с помощью предусловий Code Contract. В целом совет очень дельный, жаль, что я часто его сам избегаю.
А вот в примере "Item 29: Enhance Constructed Types with Extension Methods" из самого начала примера кроется ошибка в том что, класс для Extension методов, в котором они реализованы, должен быть статическим. Возможно, это просто опечатка в книге.
Совет, в котором автор предпочитает использовать явное указание типа переменной, вместо var, так как в некоторых случаях это улучшает читабельность кода. Мой взгляд в этом подходе полностью отличается от мнения автора. Во-первых, я пользуюсь таким замечательным инструментом, как Resharper, который подставляет var, вместо явного указания типа. Его можно настроить так, что var не будет заменяться вместо явного указания типа. Но давайте посмотрим на ту проблему, которую описывает автор, и является ли то, о чем говорит автор, проблемой как таковой. 
var f = GetMagicNumber();
var total = 100 * f / 6;
Console.WriteLine("Type: {0}, Value: {1}",
       total.GetType().Name, total);
Код функции GetMagicNumber() приведен ниже.
private static decimal GetMagicNumber()
{
       return 10;
}
Возникает вопрос: чем в нашем примере поможет явное указание типа? Ясностью кода? Дело в том, что если указать явно тип возвращаемого аргумента из функции, это не исправит ситуацию. А если не объявлять промежуточную переменную f, а использовать сразу результат, который возвращает функция GetMagicNumber(), читабельность это не добавит. Автор пытается найти проблему не там. Использование var позволяет в некоторых случаях упростить написание кода и способствует лучшему его пониманию.
static void Main(string[] args)
{
       IEnumerable<int> numbers = Enumerable.Range(0, 20);
       IEnumerable<int> odds = numbers.Where(x => x%2 != 0);

       foreach (var odd in odds)
       {
             Console.WriteLine(odd);
       }

       Console.ReadKey();
}
Чем нам как разработчикам помогут длинные названия типов IEnumerable<int>? А если, например, подлиннее запись:
IEnumerable<Tuple<decimal, decimal, decimal>> prices = new List<Tuple<decimal, decimal, decimal>>();
Разве запись такого вида не будет читаться лучше?
var prices = new List<Tuple<decimal, decimal, decimal>>();
Этот совет имеет две стороны медали. Явное указание типа переменной позволит вам более ясно видеть, какой результат возвращает функция. Но если вы пишите нормально код, то вам это не понадобится, так как в вашей IDE должна быть подсветка синтаксиса и подсказки. В той же Visual Studio это ложится на IntelliSense. Но если вы пишете код в блокноте, то использование var наоборот позволит вам сделать меньше ошибок в вашем проекте.
В совете "Item 32: Create Composable APIs for External Components" автор рассказывает об extension methods и приводит пример, в котором говорится, что у данного метода будет не такое поведение, которое ожидает автор. Ниже приведен аналогичный пример.
public static class StringExtensions
{
       public static int GetLength(this string source)
       {
             return source == null ? 0 : source.Length;
       }
}
Использование метода приведено ниже.
string a = null;
var lenght = a.GetLength();

Console.WriteLine(lenght);
Автор статьи утверждает, что мы получим NullRefferenceException до того, как будет вызван метод GetLength.
Anyone using this method will be confused by its behavior. The .NET runtime checks for null object pointers and generates a null reference exception before any instance methods can be called. Extension methods specifically designed to work with null violate that principle and lead to code that will be hard to maintain or extend over time.
К сожалению, это полнейшая бессмыслица. Возможно, на момент написания книги автор пользовался не полноценным компилятором языка C# 3.0. Посмотрите, что нам сгенерирует IL.
.method private hidebysig static
    void Main (
        string[] args
    ) cil managed
{
    // Method begins at RVA 0x2050
    // Code size 27 (0x1b)
    .maxstack 1
    .entrypoint
    .locals init (
        [0] string a,
        [1] int32 lenght
    )

    IL_0000: ldnull
    IL_0001: stloc.0
    IL_0002: ldloc.0
    IL_0003: call int32 ParseLoadStatistic.StringExtensions::GetLength(string)
    IL_0008: stloc.1
    IL_0009: ldloc.1
    IL_000a: call void [mscorlib]System.Console::WriteLine(int32)
    IL_000f: call void [mscorlib]System.Console::WriteLine()
    IL_0014: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
    IL_0019: pop
    IL_001a: ret
// end of method Program::Main
И, соответственно, реализация самого метода GetLength():
.method public hidebysig static
    int32 GetLength (
        string source
    ) cil managed
{
    .custom instance void [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x21b0
    // Code size 12 (0xc)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: brfalse.s IL_000a

    IL_0003: ldarg.0
    IL_0004: callvirt instance int32 [mscorlib]System.String::get_Length()
    IL_0009: ret

    IL_000a: ldc.i4.0
    IL_000b: ret
// end of method StringExtensions::GetLength
Если вы понимаете немного IL код, то увидите, что у нас объявлено две локальные переменные a и length. Для первой устанавливаем значение Ldnull, которое складывает нулевую ссылку в стек. Чтобы это протестировать, я указал Visual Studio 2012 использовать .NET Framework 3.5. Но предположим, что студия взяла компилятор более новый, например, C# 4.0 или C# 5.0. И запустим все это добро с командной строки. Для этого запустим командную строку, и укажем путь к csc.exe. Этот путь будет что-то вроде этого: C:\Windows\Microsoft.NET\Framework\v3.5.
c:\Windows\Microsoft.NET\Framework\v3.5>csc.exe /t:exe /out:C:\Work\Projects\ParseLoadStatistic\ParseLoadStatistic\MyProgram.exe C:\Work\Projects\ParseLoadStatistic\ParseLoadStatistic\Program.cs
Запуск у меня выглядел следующим образом (с явным указанием пути к файла и созданием консольного приложения):
// mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Так что можете не переживать: методы расширения у вас будут работать. Предполагаю, у автора была какая-то древняя версия компилятора, иначе не могу объяснить эту ошибку.  
Почему-то больше всего мне не нравится надуманность пояснений и примеров, когда мысль, которую пытается донести читателям автор, теряется. В совете "Item 40. Distinguish Early from Deferred Execution" метод преподнесения примеров автором вовсе отбивает всякое понимание сути. Рассмотрим подобный пример, приведенный в данном совете.
class Program
{
    static void Main(string[] args)
    {
        var answer = DoStuff(MethodOne(), MethodTwo(), MethodThree());
        var answer2 = DoStuff(() => MethodOne(), () => MethodTwo(), () => MethodThree());
    }

    public static int MethodOne()
    {
        return 1;
    }

    public static int MethodTwo()
    {
        return 2;
    }

    public static int MethodThree()
    {
        return 3;
    }

    public static int DoStuff(int a, int b, int c)
    {
        return a + b + c;
    }
}
Комментарий автора согласно второго метода:
Deferred execution, in which you use lambdas and query expressions, completely changes this process and may pull the rug out from under you. The following line of code seems to do the same thing as the foregoing example, but you'll soon see that there are important differences:
var answer2 = DoStuff(() => MethodOne(), () => MethodTwo(), () => MethodThree());
Автор пишет, что метод приведенный выше делает практически то же, что и метод
var answer = DoStuff(MethodOne(), MethodTwo(), MethodThree());
После первого просмотра этого примера первой мыслью было "Это же не заработает". Поясню причину: метод принимает для нашего примера целые числа, для того чтобы он смог заработать с лямбда-выражениями, нужно явно использовать приведение. Для приведенного примера мы получим ошибку:
Error 1 Cannot convert lambda expression to type 'int' because it is not a delegate type
Рассмотрим два способа, чтобы этот способ заработал.
static void Main(string[] args)
{
    //Variant 1
    var answer = DoStuff(MethodOne(), MethodTwo(), MethodThree());
    //Variant 2
    Func<int> methodOne = () => MethodOne();
    var answer2 = DoStuff(methodOne(),
        ((Func<int>)MethodTwo)(),
        ((Func<int>)MethodThree)());
}
Я привел два способа, чтобы данный подход заработал:
Func<int> methodOne = () => MethodOne();
и явное приведение через cast:
((Func<int>)MethodTwo)()
Первый способ можно записать еще более короче:
Func<int> methodOne = MethodOne;
Возможно, автор хотел показать то, как в метод передавать функции. Тогда наша функция DoStuff примет следующий вид:
public static int DoStuff(Func<int> funcOne,
    Func<int> funcTwo,
    Func<int> funcThree)
{
    return funcOne() + funcTwo() + funcThree();
}
Но в таком случае использование примера автора тоже изменится:
static void Main(string[] args)
{
    //Variant 1
    var answer = DoStuff(MethodOne, MethodTwo, MethodThree);
    //Variant 2
    Func<int> methodOne = MethodOne;
    var answer2 = DoStuff(() => MethodOne(),
        () => MethodTwo(),
        () => MethodThree());
}
Как видим, поменялся вариант номер 1, а также вариант 2 стал похож на пример, приведенный автором. Также автор приводит пример с кешированием значений с использованием такого примера.  Цитата с автора:
Finally, in some cases, you may find that a mixture of the two strategies will work the best. You may find that caching sometimes provides the most efficiency. In those cases, you can create a delegate that returns the cached value.
int methodOne = MethodOne();
var answer2 = DoStuff(() => methodOne,
    () => MethodTwo(),
    () => MethodThree());
Дело в том, что данное кеширование разворачивается в целый отдельный класс.
// Nested Types
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass5'
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Fields
        .field public int32 methodOne

        // Methods
        .method public hidebysig specialname rtspecialname
            instance void .ctor () cil managed
        {
            // Method begins at RVA 0x2050
            // Code size 7 (0x7)
            .maxstack 8

            IL_0000: ldarg.0
            IL_0001: call instance void [mscorlib]System.Object::.ctor()
            IL_0006: ret
        } // end of method '<>c__DisplayClass5'::.ctor

        .method public hidebysig
            instance int32 '<Main>b__0' () cil managed
        {
            // Method begins at RVA 0x2058
            // Code size 11 (0xb)
            .maxstack 1
            .locals init (
                [0] int32 CS$1$0000
            )

            IL_0000: ldarg.0
            IL_0001: ldfld int32 MoreEffectiveCSharp.Program/'<>c__DisplayClass5'::methodOne
            IL_0006: stloc.0
            IL_0007: br.s IL_0009

            IL_0009: ldloc.0
            IL_000a: ret
        } // end of method '<>c__DisplayClass5'::'<Main>b__0'

    } // end of class <>c__DisplayClass5
Не очень удачная оптимизация, учитывая тот факт, что в следующем совете автор дает совет об избегании захватывания ресурсов в лямбда-выражениях. Этот совет содержит, вероятно, наибольшее количество ошибок в примерах и ошибок с трактовкой понятий языка C#.

Итоги
В целом книга оставила у меня двойственное впечатление. С одной стороны, хорошо описанный материал, который не встретишь собранным в одном источнике. С другой стороны, обилие ошибок и часть примеров, которые непонятным образом фигурируют и относятся к рассмотренному совету. Также на меня довольно удручающе влияли некоторые советы автора, множество из которых я бы не согласился применять в реальном проекте. Если сравнивать книги "More Effective C#" и "Effective C#", то вторая книга "Effective C#" получилась намного лучше. В книге не узнал ничего нового для себя. Возможно, для некоторых разработчиков книга будет неплохим источником, так как она уникальна в своем роде. В целом оценку данной книге ставлю "3" по пятибалльной шкале. Обзор книги получился большим, так как рассматривал примеры, приведенные автором, с учетом ошибок.  

No comments:

Post a Comment