Оценка экспрессии и ходьба по дереву с использованием полиморфизма? (аля Стив Йегге)

Когда вы объявляете ссылочную переменную (т. е. объект), вы действительно создаете указатель на объект. Рассмотрим следующий код, в котором вы объявляете переменную примитивного типа int:

int x;
x = 10;

В этом примере переменная x является int, и Java инициализирует ее для 0. Когда вы назначаете его 10 во второй строке, ваше значение 10 записывается в ячейку памяти, на которую указывает x.

Но когда вы пытаетесь объявить ссылочный тип, произойдет что-то другое. Возьмите следующий код:

Integer num;
num = new Integer(10);

Первая строка объявляет переменную с именем num, но она не содержит примитивного значения. Вместо этого он содержит указатель (потому что тип Integer является ссылочным типом). Поскольку вы еще не указали, что указать на Java, он устанавливает значение null, что означает «Я ничего не указываю».

Во второй строке ключевое слово new используется для создания экземпляра (или создания ) объекту типа Integer и переменной указателя num присваивается этот объект. Теперь вы можете ссылаться на объект, используя оператор разыменования . (точка).

Exception, о котором вы просили, возникает, когда вы объявляете переменную, но не создавали объект. Если вы попытаетесь разыменовать num. Перед созданием объекта вы получите NullPointerException. В самых тривиальных случаях компилятор поймает проблему и сообщит вам, что «num не может быть инициализирован», но иногда вы пишете код, который непосредственно не создает объект.

Например, вы можете имеют следующий метод:

public void doSomething(SomeObject obj) {
   //do something to obj
}

В этом случае вы не создаете объект obj, скорее предполагая, что он был создан до вызова метода doSomething. К сожалению, этот метод можно вызвать следующим образом:

doSomething(null);

В этом случае obj имеет значение null. Если метод предназначен для того, чтобы что-то сделать для переданного объекта, целесообразно бросить NullPointerException, потому что это ошибка программиста, и программисту понадобится эта информация для целей отладки.

Альтернативно, там могут быть случаи, когда цель метода заключается не только в том, чтобы работать с переданным в объекте, и поэтому нулевой параметр может быть приемлемым. В этом случае вам нужно будет проверить нулевой параметр и вести себя по-другому. Вы также должны объяснить это в документации. Например, doSomething может быть записано как:

/**
  * @param obj An optional foo for ____. May be null, in which case 
  *  the result will be ____.
  */
public void doSomething(SomeObject obj) {
    if(obj != null) {
       //do something
    } else {
       //do something else
    }
}

Наконец, Как определить исключение & amp; причина использования Трассировки стека

26
задан 14 revs, 3 users 51% 31 August 2008 в 20:18
поделиться

15 ответов

Полиморфная прогулка по дереву , версия Python

#!/usr/bin/python

class Node:
    """base class, you should not process one of these"""
    def process(self):
        raise('you should not be processing a node')

class BinaryNode(Node):
    """base class for binary nodes"""
    def __init__(self, _left, _right):
        self.left = _left
        self.right = _right
    def process(self):
        raise('you should not be processing a binarynode')

class Plus(BinaryNode):
    def process(self):
        return self.left.process() + self.right.process()

class Minus(BinaryNode):
    def process(self):
        return self.left.process() - self.right.process()

class Mul(BinaryNode):
    def process(self):
        return self.left.process() * self.right.process()

class Div(BinaryNode):
    def process(self):
        return self.left.process() / self.right.process()

class Num(Node):
    def __init__(self, _value):
        self.value = _value
    def process(self):
        return self.value

def demo(n):
    print n.process()

demo(Num(2))                                       # 2
demo(Plus(Num(2),Num(5)))                          # 2 + 3
demo(Plus(Mul(Num(2),Num(3)),Div(Num(10),Num(5)))) # (2 * 3) + (10 / 2)

Тесты просто строят двоичные деревья с помощью конструкторов.

структура программы:

абстрактный базовый класс: Node

  • все узлы наследуют от этого класса

абстрактный базовый класс: BinaryNode

  • все бинарные операторы наследуют от этого класса.
  • метод процесса выполняет вычисление выражения и возвращает результат

классы бинарных операторов: Plus, Minus, Mul, Разделите

  • два дочерних узла, по одному для левых и правых подвыражений

Числовой класс: Num

  • содержит лист-узел числовое значение, например 17 или 42
11
ответ дан Mark Harrison 31 August 2008 в 20:18
поделиться

Я как бы собрал это консольное приложение на c # в качестве доказательства концепции. Чувствую, что это могло бы быть намного лучше (что оператор switch в GetNode немного неуклюжий (потому что там я попал в пустую строку, пытаясь отобразить имя класса для оператора)). Любые предложения о том, как это можно улучшить, очень приветствуются.

using System;

class Program
{
    static void Main(string[] args)
    {
        string expression = "(((3.5 * 4.5) / (1 + 2)) + 5)";
        Console.WriteLine(string.Format("{0} = {1}", expression, new Expression.ExpressionTree(expression).Value));
        Console.WriteLine("\nShow's over folks, press a key to exit");
        Console.ReadKey(false);
    }
}

namespace Expression
{
    // -------------------------------------------------------

    abstract class NodeBase
    {
        public abstract double Value { get; }
    }

    // -------------------------------------------------------

    class ValueNode : NodeBase
    {
        public ValueNode(double value)
        {
            _double = value;
        }

        private double _double;
        public override double Value
        {
            get
            {
                return _double;
            }
        }
    }

    // -------------------------------------------------------

    abstract class ExpressionNodeBase : NodeBase
    {
        protected NodeBase GetNode(string expression)
        {
            // Remove parenthesis
            expression = RemoveParenthesis(expression);

            // Is expression just a number?
            double value = 0;
            if (double.TryParse(expression, out value))
            {
                return new ValueNode(value);
            }
            else
            {
                int pos = ParseExpression(expression);
                if (pos > 0)
                {
                    string leftExpression = expression.Substring(0, pos - 1).Trim();
                    string rightExpression = expression.Substring(pos).Trim();

                    switch (expression.Substring(pos - 1, 1))
                    {
                        case "+":
                            return new Add(leftExpression, rightExpression);
                        case "-":
                            return new Subtract(leftExpression, rightExpression);
                        case "*":
                            return new Multiply(leftExpression, rightExpression);
                        case "/":
                            return new Divide(leftExpression, rightExpression);
                        default:
                            throw new Exception("Unknown operator");
                    }
                }
                else
                {
                    throw new Exception("Unable to parse expression");
                }
            }
        }

        private string RemoveParenthesis(string expression)
        {
            if (expression.Contains("("))
            {
                expression = expression.Trim();

                int level = 0;
                int pos = 0;

                foreach (char token in expression.ToCharArray())
                {
                    pos++;
                    switch (token)
                    {
                        case '(':
                            level++;
                            break;
                        case ')':
                            level--;
                            break;
                    }

                    if (level == 0)
                    {
                        break;
                    }
                }

                if (level == 0 && pos == expression.Length)
                {
                    expression = expression.Substring(1, expression.Length - 2);
                    expression = RemoveParenthesis(expression);
                }
            }
            return expression;
        }

        private int ParseExpression(string expression)
        {
            int winningLevel = 0;
            byte winningTokenWeight = 0;
            int winningPos = 0;

            int level = 0;
            int pos = 0;

            foreach (char token in expression.ToCharArray())
            {
                pos++;

                switch (token)
                {
                    case '(':
                        level++;
                        break;
                    case ')':
                        level--;
                        break;
                }

                if (level <= winningLevel)
                {
                    if (OperatorWeight(token) > winningTokenWeight)
                    {
                        winningLevel = level;
                        winningTokenWeight = OperatorWeight(token);
                        winningPos = pos;
                    }
                }
            }
            return winningPos;
        }

        private byte OperatorWeight(char value)
        {
            switch (value)
            {
                case '+':
                case '-':
                    return 3;
                case '*':
                    return 2;
                case '/':
                    return 1;
                default:
                    return 0;
            }
        }
    }

    // -------------------------------------------------------

    class ExpressionTree : ExpressionNodeBase
    {
        protected NodeBase _rootNode;

        public ExpressionTree(string expression)
        {
            _rootNode = GetNode(expression);
        }

        public override double Value
        {
            get
            {
                return _rootNode.Value;
            }
        }
    }

    // -------------------------------------------------------

    abstract class OperatorNodeBase : ExpressionNodeBase
    {
        protected NodeBase _leftNode;
        protected NodeBase _rightNode;

        public OperatorNodeBase(string leftExpression, string rightExpression)
        {
            _leftNode = GetNode(leftExpression);
            _rightNode = GetNode(rightExpression);

        }
    }

    // -------------------------------------------------------

    class Add : OperatorNodeBase
    {
        public Add(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        {
        }

        public override double Value
        {
            get
            {
                return _leftNode.Value + _rightNode.Value;
            }
        }
    }

    // -------------------------------------------------------

    class Subtract : OperatorNodeBase
    {
        public Subtract(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        {
        }

        public override double Value
        {
            get
            {
                return _leftNode.Value - _rightNode.Value;
            }
        }
    }

    // -------------------------------------------------------

    class Divide : OperatorNodeBase
    {
        public Divide(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        {
        }

        public override double Value
        {
            get
            {
                return _leftNode.Value / _rightNode.Value;
            }
        }
    }

    // -------------------------------------------------------

    class Multiply : OperatorNodeBase
    {
        public Multiply(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        {
        }

        public override double Value
        {
            get
            {
                return _leftNode.Value * _rightNode.Value;
            }
        }
    }
}
0
ответ дан Wilfred Knievel 31 August 2008 в 20:18
поделиться

Я написал такой парсер с некоторыми базовыми приемами, такими как Infix -> RPN и Shunting Yard и Обходы деревьев . Вот реализация, которую я придумал .
Он написан на C ++ и компилируется как в Linux, так и в Windows.
Любые предложения / вопросы приветствуются.

Итак, давайте попробуем решить проблему всеми тремя способами. Как перейти от арифметического выражения (например, в строке), такого как "2 + (2)", к дереву выражений с использованием каскадных if, таблицы указателей на функции и / или полиморфизма?

Это интересно, но я не думаю, что это относится к области объектно-ориентированного программирования ... Я думаю, что это больше связано с методами синтаксического анализа .

0
ответ дан 2 revs 31 August 2008 в 20:18
поделиться

следует использовать функциональный язык Imo. Деревья сложнее представлять и манипулировать на ОО-языках.

0
ответ дан Brian Leahy 31 August 2008 в 20:18
поделиться

Я думаю, что вопрос в том, как написать синтаксический анализатор, а не оценщик. Или, скорее, как создать дерево выражений из строки.

Операторы case, которые возвращают базовый класс, точно не учитываются.

Базовая структура «полиморфного» решения (это еще один способ сказать, мне все равно, с чем вы это строите, я просто хочу расширить его с помощью переписывания наименьшего количества кода) - десериализация объекта иерархия из потока с (динамическим) набором известных типов.

Суть реализации полиморфного решения заключается в том, чтобы иметь способ создания объекта выражения из сопоставителя шаблонов, вероятно, рекурсивного. То есть сопоставить BNF или аналогичный синтаксис с фабрикой объектов.

1
ответ дан Tim Cooper 31 August 2008 в 20:18
поделиться

Хм ... Я не думаю, что вы можете написать для этого нисходящий парсер без возврата назад, так что это должен быть своего рода парсер с уменьшением сдвига. LR (1) или даже LALR, конечно, будут отлично работать со следующим (специальным) определением языка:

Start -> E1
E1 -> E1 + E1 | E1-E1
E1 -> E2 * E2 | E2 / E2 | E2
E2 -> число | (E1)

Разделение его на E1 и E2 необходимо для сохранения приоритета * и / над + и -.

Но вот как бы я это сделал, если бы мне пришлось писать синтаксический анализатор вручную:

  • Два стека, один хранит узлы дерева в качестве операндов, а другой хранит операторы
  • Читайте ввод слева направо, делайте листовые узлы чисел и помещайте их в стек операндов.
  • Если у вас есть> = 2 операнда в стеке, всплывают 2, объедините их с верхним оператором в стеке операторов и перенесите эту структуру обратно в дерево операндов, , если только
  • Следующий оператор имеет более высокий приоритет, чем тот, который в настоящее время находится на вершине стека.

Это оставляет нам проблему обработки скобок. Одним из элегантных (я думал) решений является сохранение приоритета каждого оператора в виде числа в переменной. Итак, изначально,

  • int plus, minus = 1;
  • int mul, div = 2;

Теперь каждый раз, когда вы видите увеличение левой скобки все эти переменные на 2, и каждый раз, когда вы видите правую скобку, уменьшайте все переменные на 2.

Это гарантирует, что + in 3 * (4 + 5) имеет более высокий приоритет, чем *, и 3 * 4 не будет помещен в стек. Вместо этого он будет ждать 5, нажмите 4 + 5, затем нажмите 3 * (4 + 5).

2
ответ дан anukool_j 31 August 2008 в 20:18
поделиться

Я не буду давать ответ, но Стандартное Плохое Решение включает в себя использование переключателя или определения случая (или просто хорошего старомодного каскадного ввода-вывода). Немного лучшее решение предполагает использование таблицы указателей функций, а, вероятно, лучшее решение предполагает использование полиморфизма.

Последние двадцать лет эволюции интерпретаторов можно рассматривать как движение по другому пути - полиморфизм (например, наивные метациркулярные интерпретаторы Smalltalk) на функции указателей (реализации наивного lisp, многопоточного кода, C ++) для переключения ( наивные интерпретаторы байт-кода), а затем и далее в JIT и т. д., которые либо требуют очень больших классов, либо (в однополиморфных языках) двойной диспетчеризации, что сводит полиморфизм к регистру типов, и вы возвращаетесь на этап один. Какое определение 'best' используется здесь?

Для простых вещей полиморфное решение в порядке - вот то, что я сделал ранее , но либо стек и байт-код / ​​переключатель, либо использование компилятора среды выполнения обычно лучше, если вы, скажем, строите функцию с несколькими тысячами точек данных.

2
ответ дан Pete Kirkham 31 August 2008 в 20:18
поделиться

Представление узлов

Если мы хотим включить круглые скобки, нам нужно 5 видов узлов:

  • двоичные узлы: Добавить минус Mul Div
    у них есть два потомка, левая и правая стороны

         +
        / \
    node   node
    
  • узел для хранения значения: Val
    нет дочерних узлов, только числовое значение

  • узел для отслеживания паренов: Paren
    один дочерний узел для подвыражения

        ( )
         |
        node
    

Для Полиморфное решение, нам нужно иметь такие классовые отношения:

  • Node
  • BinaryNode: наследовать от Node
  • Plus: наследовать от Binary Node
  • Минус: наследование от двоичного узла
  • Mul: наследование от двоичного узла
  • Div: наследование от двоичного узла
  • Значение: наследование от узла
  • Paren: наследовать от узла

Существует виртуальная функция для всех узлов, называемая eval (). Если вы вызовете эту функцию, она вернет значение этого подвыражения.

4
ответ дан Mark Harrison 31 August 2008 в 20:18
поделиться

Это входит в теорию синтаксического анализа / компилятора, которая является своего рода кроличьей ношей ... Книга Дракона является стандартным текстом для построения компилятора, и доводит это до крайности. В этом конкретном случае вы хотите создать контекстную грамматику без контекста для базовой арифметики, а затем использовать эту грамматику для анализа абстрактного синтаксического дерева . Затем вы можете перебирать дерево, уменьшая его снизу вверх (именно в этот момент вы примените оператор полиморфизма / указателей на функции / переключателя, чтобы уменьшить дерево).

Я нашел эти заметки невероятно полезными в теории компиляции и парсинга.

4
ответ дан eplawless 31 August 2008 в 20:18
поделиться

Проблема, я думаю, в том, что нам нужно разобрать perentheses, и все же они не являются бинарным оператором? Должны ли мы взять (2) в качестве одного токена, который оценивается в 2?

Паренсы не должны отображаться в дереве выражений, но они влияют на его форму. Например, дерево для (1 + 2) +3 отличается от 1+ (2 + 3):

    +
   / \
  +   3
 / \
1   2

против

    +
   / \
  1   +
     / \
    2   3

Скобки являются «подсказкой» для синтаксический анализатор (например, per superjoe30, чтобы «рекурсивно спуститься»)

4
ответ дан Chris Conway 31 August 2008 в 20:18
поделиться

Re: Джастин

Я думаю, что дерево выглядело бы примерно так:

  +
 / \
2  ( )
    |
    2

По сути, у вас был бы «eval» узел, который просто оценивает дерево под ним , Это было бы оптимизировано так:

  +
 / \
2   2

В этом случае паренсы не требуются и ничего не добавляют. Они ничего не добавляют логически, поэтому они просто ушли.

1
ответ дан Herms 31 August 2008 в 20:18
поделиться

Или возможно это - реальный вопрос: как можно представить (2) как BST? Это - часть, которая сбивает меня с толку.

Рекурсия.

0
ответ дан andrewrk 31 August 2008 в 20:18
поделиться

Строковый Токенизатор + LL (1) Синтаксический анализатор даст Вам дерево выражений... полиморфизм, путь мог бы связать абстрактный Арифметический класс с, "оценивают (a, b)" функцию, которая переопределяется для каждого из включенных операторов (Дополнение, Вычитание и т.д.) для возврата соответствующего значения, и дерево содержит Целые числа и Арифметические операторы, которые могут быть оценены сообщением (?) - заказывают обход дерева.

2
ответ дан eplawless 31 August 2008 в 20:18
поделиться
  • 1
    Это также добавляет абсолютно ненужный вызов функции. – modiX 11 April 2019 в 14:59

Как люди упоминали ранее, когда Вы используете деревья выражений parens, не необходимы. Порядок операций становится тривиальным и очевидным при рассмотрении дерева выражений. parens являются подсказками к синтаксическому анализатору.

, В то время как принятый ответ является решением одной половины проблемы, другая половина - на самом деле парсинг выражения - является все еще нерешенной. Как правило, эти виды проблем могут быть решены с помощью синтаксический анализатор с рекурсивным спуском . Запись такого синтаксического анализатора часто является забавным осуществлением, но большинство современные инструменты для синтаксического анализа естественных языков абстрагирует это далеко для Вас.

синтаксический анализатор также значительно тяжелее при разрешении чисел с плавающей точкой в строке. Я должен был создать DFA для принятия чисел с плавающей точкой в C - это была очень кропотливая и подробная задача. Помните, допустимые плавающие точки включают: 10, 10., 10.123, 9.876e-5, 1.0f.025, и т.д. Я предполагаю, что некоторое разрешение от этого (в пользу simplicty и краткости) было сделано в интервью.

0
ответ дан FreeMemory 31 August 2008 в 20:18
поделиться

@Justin:

Взгляд на мое примечание по представлению узлов. Если Вы используете ту схему, то

2 + (2)

может быть представлен как

           .
          / \
         2  ( )
             |
             2
0
ответ дан Mark Harrison 31 August 2008 в 20:18
поделиться
  • 1
    +1! Это точно, что я искал. Это делает намного легче зеркально отразить bool на основе другого bool (" abcd".Contains (" s") ^ = someNegateBool) – Trafz 16 June 2017 в 00:20
Другие вопросы по тегам:

Похожие вопросы: