Что можно сделать, чтобы улучшить читаемость математически ориентированного кода на C #, Java и т.п.? [закрыто]

16

Как программист C и программист C #, одна вещь, которая мне не нравится в C #, - это подробные математические функции. Например, каждый раз, когда вам нужно будет использовать функцию Sin, cosine или power, вы должны будете добавить статический класс Math. Это приводит к очень длинному коду, когда само уравнение довольно простое. Проблема становится еще хуже, если вам нужно типизировать типы данных. В результате, на мой взгляд, читаемость страдает. Например:

double x =  -Math.Cos(X) * Math.Sin(Z) + Math.Sin(X) * Math.Sin(Y) * Math.Cos(Z);

В отличие от просто

double x = -cos(X) * sin(Z) + sin(X) * sin(Y) * cos(Z);

Это также имеет место в других языках, таких как Java.

Я не уверен, что этот вопрос действительно имеет решение, но я хотел бы знать, есть ли какие-нибудь хитрости, которые используют программисты на C # или Java для улучшения читаемости кода Math. Однако я понимаю, что C # / Java / и т. Д. не ориентированные на математику языки, такие как MATLAB или подобные, так что это имеет смысл. Но иногда нужно было бы написать математический код, и было бы здорово, если бы он мог сделать его более читабельным.

9a3eedi
источник
Я не знаю ничего конкретного, но вы, вероятно, могли бы найти библиотеку алгебры, которая позволила бы вам определять математические функции со строками, хотя это могло бы привести к некоторому снижению производительности.
raptortech97
7
Вы беспокоитесь о некотором лишнем многословии, но счастливо скрываете знак «+» среди «*» с унарными операторами - все без скобок - я подозреваю, что у вас неправильные приоритеты.
mattnz
1
Это был просто пример, но хороший момент
9a3eedi
6
В C # 6.0, вы сможете написать: using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
svick

Ответы:

14

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

class MathHeavy
{
    private double sin(double x) { return Math.sin(x); }
    private double cos(double x) { return Math.cos(x); }

    public double foo(double x, double y)
    {
        return sin(x) * cos(y) - cos(x) * sin(y);
    }
}

Вы также можете создавать функции, которые объединяют общие математические операции в отдельные операции. Это сведет к минимуму количество случаев, когда функции похожи sinи cosпоявляются в вашем коде, тем самым делая неловкость вызова глобальных статических функций менее заметной. Например:

public Point2D rotate2D(double angle, Point2D p)
{
    double x = p.x * Math.cos(angle) - p.y * Math.sin(angle);
    double y = p.x * Math.sin(angle) + p.y * Math.cos(angle);

    return new Point2D(x, y)
}

Вы работаете на уровне точек и поворотов, и основные функции триггера скрыты.

Рэндалл Кук
источник
... почему я не подумал об этом :)
9a3eedi
Я отметил это как правильный ответ, потому что это кроссплатформенное решение, которое достаточно просто. Другие решения также верны. Я действительно не могу поверить, что я не думал об этом, хотя :) это слишком очевидно
9a3eedi
31

В Java есть много инструментов, позволяющих сделать некоторые вещи менее многословными, вы просто должны знать о них. В этом случае полезен staticимпорт ( страница учебника , википедия ).

В таком случае,

import static java.lang.Math.*;

class Demo {
    public static void main (String[] args) {
        double X = 42.0;
        double Y = 4.0;
        double Z = PI;

        double x =  -cos(X) * sin(Z) + sin(X) * sin(Y) * cos(Z);
        System.out.println(x);
    }
}

работает довольно красиво ( ideone ). Немного тяжело выполнять статический импорт всех классов Math, но если вы много занимаетесь математикой, то это может потребоваться.

Статический импорт позволяет импортировать статическое поле или метод в пространство имен этого класса и вызывать его без указания имени пакета. Вы часто это в тех случаях , тест JUnit , где import static org.junit.Assert.*;находится , чтобы получить все утверждает доступны.


источник
Отличный ответ. Я не знал об этой функции. В какой версии Java это возможно?
9a3eedi
@ 9a3eedi Впервые он был доступен в Java 1.5.
Хорошая техника. Мне это нравится. +1.
Рэндалл Кук
1
@RandallCook В Java 1.4 дня люди делали что-то вроде, public interface Constants { final static public double PI = 3.14; }а затем public class Foo implements Constantsво всех классах, чтобы получить доступ к константам в интерфейсе. Это сделало большой беспорядок. Итак, в версии 1.5 был добавлен статический импорт, позволяющий использовать определенные константы и статические функции без необходимости реализации интерфейса.
3
Вы можете выборочно импортировать определенные функцииimport static java.lang.Math.cos;
трещотка урод
5

С C # 6.0 вы можете использовать функцию статического импорта.

Ваш код может быть:

using static System.Math;
using static System.Console;
namespace SomeTestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            double X = 123;
            double Y = 5;
            double Z = 10;
            double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);
            WriteLine(x); //Without System, since it is imported 
        }
    }
}

См. Статическое использование операторов (AC # 6.0 Language Preview)

Еще одна особенность C # 6.0 «синтаксический сахар» - введение использования static. С помощью этой функции можно исключить явную ссылку на тип при вызове статического метода. Кроме того, использование static позволяет вводить только методы расширения для определенного класса, а не все методы расширения в пространстве имен.

РЕДАКТИРОВАТЬ: Начиная с Visual Studio 2015, CTP выпущен в январе 2015 года, статический импорт требует явного ключевого слова static. нравиться:

using static System.Console;
Хабиб
источник
4

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

С помощью инструмента создания DSL, такого как Xtext , вы можете определить свою собственную упрощенную математическую грамматику, которая, в свою очередь, может сгенерировать класс, содержащий представление ваших формул на Java (или любом другом языке).

DSL Expression:

domain GameMath {
    formula CalcLinearDistance(double): sqrt((x2 - x1)^2 + (y2 - y1)^2)
}

Сгенерированный выход:

public class GameMath {
    public static double CalcLinearDistance(int x1, int x2, int y1, int y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    }
}

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

Dan1701
источник
8
Да, в целом и по определению DSL может быть полезен при работе в определенном домене. Однако, если этот DSL не существует или если он не соответствует потребностям, вы должны поддерживать его, что может быть проблематично. Кроме того, для конкретного вопроса («Как я могу использовать sin, cos,… методы / функции без написания каждого класса Math каждый раз»), DSL может быть слишком большим решением.
mgoeminne
4

В C # вы можете использовать методы расширения.

Ниже вы можете прочитать довольно хорошо, как только вы привыкнете к записи «postfix»:

public static class DoubleMathExtensions
{
    public static double Cos(this double n)
    {
        return Math.Cos(n);
    }

    public static double Sin(this double n)
    {
        return Math.Sin(n);
    }

    ...
}

var x =  -X.Cos() * Z.Sin() + X.Sin() * Y.Sin() * Z.Cos();

К сожалению, при работе с отрицательными числами приоритеты операторов немного усложняют ситуацию. Если вы хотите рассчитать Math.Cos(-X)вместо -Math.Cos(X)вас нужно будет заключить число в круглые скобки:

var x = (-X).Cos() ...
rjnilsson
источник
1
Между прочим, это было бы хорошим вариантом использования для свойств расширения и даже допустимым вариантом использования для злоупотребления свойствами в качестве методов!
Йорг Миттаг
Это то, что я думал. x.Sin()потребовалась бы некоторая корректировка, но я злоупотребляю методами расширения, и это будет, лично я, моим первым желанием.
WernerCD,
2

C #: Вариант ответа Рэндалла Кука , который мне нравится, потому что он поддерживает математический «вид» кода больше, чем методы расширения, состоит в том, чтобы использовать оболочку, но использовать ссылки на функции для вызовов, а не заключать их в оболочку. Я лично думаю, что это делает код более чистым, но в основном делает то же самое.

Я запустил небольшую тестовую программу LINQPad, включающую упакованные функции Рэндалла, ссылки на мои функции и прямые вызовы.

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

Вот код:

void Main()
{
    MyMathyClass mmc = new MyMathyClass();

    System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

    for(int i = 0; i < 50000000; i++)
        mmc.DoStuff(1, 2, 3);

    "Function reference:".Dump();
    sw.Elapsed.Dump();      
    sw.Restart();

    for(int i = 0; i < 50000000; i++)
        mmc.DoStuffWrapped(1, 2, 3);

    "Wrapped function:".Dump();
    sw.Elapsed.Dump();      
    sw.Restart();

    "Direct call:".Dump();
    for(int i = 0; i < 50000000; i++)
        mmc.DoStuffControl(1, 2, 3);

    sw.Elapsed.Dump();
}

public class MyMathyClass
{
    // References
    public Func<double, double> sin;
    public Func<double, double> cos;
    public Func<double, double> tan;
    // ...

    public MyMathyClass()
    {
        sin = System.Math.Sin;
        cos = System.Math.Cos;
        tan = System.Math.Tan;
        // ...
    }

    // Wrapped functions
    public double wsin(double x) { return Math.Sin(x); }
    public double wcos(double x) { return Math.Cos(x); }
    public double wtan(double x) { return Math.Tan(x); }

    // Calculation functions
    public double DoStuff(double x, double y, double z)
    {
        return sin(x) + cos(y) + tan(z);
    }

    public double DoStuffWrapped(double x, double y, double z)
    {
        return wsin(x) + wcos(y) + wtan(z);
    }

    public double DoStuffControl(double x, double y, double z)
    {
        return Math.Sin(x) + Math.Cos(y) + Math.Tan(z);
    }
}

Результаты:

Function reference:
00:00:06.5952113

Wrapped function:
00:00:07.2570828

Direct call:
00:00:06.6396096
Whelkaholism
источник
1

Используйте Scala! Вы можете определять символические операторы, и вам не нужны парены для ваших методов. Это делает математику так легче интерпретировать.

Например, один и тот же расчет в Scala и Java может выглядеть примерно так:

// Scala
def angle(u: Vec, v: Vec) = (u*v) / sqrt((u*u)*(v*v))

// Java
public double angle(u: Vec, v: Vec) {
  return u.dot(v) / sqrt(u.dot(u)*v.dot(v));
}

Это складывается довольно быстро.

Рекс Керр
источник
2
Scala недоступна в CLR, только в JVM. Таким образом, это на самом деле не жизнеспособная альтернатива C #.
Бен Руджерс
@benrudgers - C # не работает на JVM, поэтому он не является жизнеспособной альтернативой Java, о которой также задавался вопрос. Вопрос не указывает, что это должен быть CLR!
Рекс Керр,
Может быть, я луддит, но два дополнительных символа для «точки» вместо «*», с преимуществом, которое делает код более понятным, кажется небольшой платой. Тем не менее, хороший ответ.
user949300