Người ta có thể làm gì để cải thiện khả năng đọc mã định hướng toán học trong C #, Java và tương tự? [đóng cửa]


16

Vừa là lập trình viên C vừa là lập trình viên C #, một trong những điều tôi không thích ở C # là cách các hàm toán học dài dòng. Ví dụ, mỗi lần bạn phải sử dụng hàm Sin, cosine hoặc hàm sức mạnh, bạn sẽ phải thêm vào lớp tĩnh Toán. Điều này dẫn đến mã rất dài khi bản thân phương trình khá đơn giản. Vấn đề thậm chí còn trở nên tồi tệ hơn nếu bạn cần đánh máy các kiểu dữ liệu. Kết quả là, theo tôi, khả năng đọc bị ảnh hưởng. Ví dụ:

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

Trái ngược với đơn giản

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

Đây cũng là trường hợp trong các langau khác như Java.

Tôi không chắc câu hỏi này có thực sự có lời giải hay không, nhưng tôi muốn biết liệu có bất kỳ thủ thuật nào mà các lập trình viên C # hoặc Java sử dụng để cải thiện khả năng đọc mã Toán học hay không. Tuy nhiên, tôi nhận ra rằng C # / Java / vv. không phải là ngôn ngữ hướng toán học như MATLAB hoặc tương tự, vì vậy nó có ý nghĩa. Nhưng đôi khi người ta vẫn cần viết mã toán học và thật tuyệt nếu người ta có thể làm cho nó dễ đọc hơn.


Tôi không biết cụ thể, nhưng có lẽ bạn có thể tìm thấy một thư viện đại số cho phép bạn xác định các hàm toán học bằng các chuỗi, mặc dù sẽ có một số hình phạt hiệu năng.
raptortech97


7
Bạn lo lắng về một chút dài dòng nhưng vẫn vui vẻ giấu một dấu '+' trong số '*' với các toán tử đơn nguyên - tất cả đều không có dấu ngoặc - Tôi nghi ngờ bạn có các ưu tiên sai.
mattnz

1
Đó chỉ là một ví dụ, nhưng điểm tốt
9a3eedi

6
Trong C # 6.0, bạn sẽ có thể viết : using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
Svick

Câu trả lời:


14

Bạn có thể định nghĩa các hàm cục bộ gọi các hàm tĩnh toàn cục. Hy vọng trình biên dịch sẽ nội tuyến các trình bao bọc, và sau đó trình biên dịch JIT sẽ tạo ra mã lắp ráp chặt chẽ cho các hoạt động thực tế. Ví dụ:

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);
    }
}

Bạn cũng có thể tạo các hàm kết hợp các phép toán phổ biến thành các phép toán đơn lẻ. Điều này sẽ giảm thiểu số lượng các trường hợp trong đó các hàm thích sincosxuất hiện trong mã của bạn, do đó làm cho sự vụng về của việc gọi các hàm tĩnh toàn cầu ít được chú ý hơn. Ví dụ:

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)
}

Bạn đang làm việc ở cấp độ điểm và phép quay, và các hàm lượng giác cơ bản bị chôn vùi.


... tại sao tôi không nghĩ về điều đó :)
9a3eedi

Tôi đã đánh dấu đây là câu trả lời đúng vì đây là một giải pháp đa nền tảng đủ đơn giản. Các giải pháp khác là chính xác là tốt. Tôi thực sự không thể tin rằng tôi đã không nghĩ về điều này mặc dù :) nó quá rõ ràng
9a3eedi

31

Trong Java, có nhiều công cụ có sẵn để làm cho một số thứ ít dài dòng hơn, bạn chỉ cần nhận thức được chúng. Một trong những hữu ích trong trường hợp này là staticnhập khẩu ( trang hướng dẫn , wikipedia ).

Trong trường hợp này,

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);
    }
}

chạy khá độc đáo ( ideone ). Sẽ hơi nặng tay khi thực hiện nhập tĩnh cho tất cả các lớp Toán, nhưng nếu bạn đang làm nhiều toán, thì nó có thể được yêu cầu.

Nhập tĩnh cho phép bạn nhập trường tĩnh hoặc phương thức vào không gian tên của lớp này và gọi nó mà không yêu cầu tên gói. Bạn thường tìm thấy điều này trong các trường hợp thử nghiệm Junit, nơi import static org.junit.Assert.*;được tìm thấy để có được tất cả các khẳng định có sẵn.


Câu trả lời tuyệt vời. Tôi đã không nhận thức được tính năng này. Phiên bản nào của Java là có thể theo?
9a3eedi

@ 9a3eedi Nó được cung cấp lần đầu tiên trong Java 1.5.

Kỹ thuật đẹp. Tôi thích nó. +1.
Randall Cook

1
@RandallCook Trong Java 1,4 ngày, mọi người sẽ làm những việc như thế public interface Constants { final static public double PI = 3.14; }và sau đó public class Foo implements Constantstrong tất cả các lớp để có quyền truy cập vào các hằng số trong giao diện. Điều này làm cho một mớ hỗn độn lớn . Vì vậy, với 1.5, nhập tĩnh đã được thêm vào để cho phép kéo các hằng số và hàm tĩnh cụ thể mà không phải thực hiện giao diện.

3
Bạn có thể nhập có chọn lọc một số chức năng nhất địnhimport static java.lang.Math.cos;
ratchet freak

5

Với C # 6.0, bạn có thể sử dụng tính năng Nhập tĩnh.

Mã của bạn có thể là:

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 
        }
    }
}

Xem: Báo cáo sử dụng tĩnh (Xem trước ngôn ngữ AC # 6.0)

Một tính năng đường cú pháp cú pháp C # 6.0 khác là giới thiệu sử dụng tĩnh. Với tính năng này, có thể loại bỏ một tham chiếu rõ ràng đến loại khi gọi một phương thức tĩnh. Hơn nữa, sử dụng static cho phép bạn chỉ giới thiệu các phương thức mở rộng trên một lớp cụ thể, thay vì tất cả các phương thức mở rộng trong một không gian tên.

EDIT: Kể từ Visual Studio 2015, CTP được phát hành vào tháng 1 năm 2015, nhập tĩnh yêu cầu từ khóa rõ ràng static. giống:

using static System.Console;

4

Ngoài các câu trả lời hay khác ở đây, tôi cũng có thể đề xuất DSL cho các tình huống có độ phức tạp toán học đáng kể (không phải trường hợp sử dụng trung bình, nhưng có lẽ một số dự án tài chính hoặc học thuật).

Với một công cụ tạo DSL như Xtext , bạn có thể xác định ngữ pháp toán học đơn giản của riêng mình, từ đó có thể tạo ra một lớp có chứa biểu diễn Java (hoặc bất kỳ ngôn ngữ nào khác) trong các công thức của bạn.

Biểu thức DSL:

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

Tạo đầu ra:

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));
    }
}

Trong một ví dụ đơn giản như vậy, lợi ích của việc tạo ra ngữ pháp và plugin Eclipse sẽ không đáng giá, nhưng đối với các dự án phức tạp hơn, nó có thể mang lại lợi ích lớn, đặc biệt là nếu DSL cho phép các doanh nhân hoặc nhà nghiên cứu học thuật duy trì tài liệu chính thức một cách thoải mái ngôn ngữ và được đảm bảo rằng công việc của họ đã được dịch chính xác sang ngôn ngữ thực hiện của dự án.


8
Có, nói chung và theo định nghĩa, DSL có thể hữu ích khi bạn làm việc trong một miền cụ thể. Tuy nhiên, nếu DSL này không tồn tại hoặc nếu nó không phù hợp với nhu cầu, bạn phải duy trì nó, điều này có thể có vấn đề. Ngoài ra, đối với câu hỏi cụ thể ("Làm cách nào tôi có thể sử dụng các phương thức / hàm sin, cos, mà không cần viết lớp Toán mỗi lần"), DSL có thể là một giải pháp quá khổ.
mgoeminne

4

Trong C # bạn có thể sử dụng các phương thức mở rộng.

Dưới đây đọc khá độc đáo khi bạn đã quen với ký hiệu "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();

Thật không may, quyền ưu tiên của nhà điều hành làm cho mọi thứ trở nên xấu hơn một chút khi xử lý các số âm ở đây. Nếu bạn muốn tính toán Math.Cos(-X)thay vì -Math.Cos(X)bạn sẽ cần phải đóng số trong ngoặc đơn:

var x = (-X).Cos() ...

1
Ngẫu nhiên, điều này sẽ tạo ra một trường hợp sử dụng tốt cho các thuộc tính mở rộng và thậm chí là trường hợp sử dụng hợp pháp để lạm dụng các thuộc tính làm phương thức!
Jörg W Mittag

Đây là những gì tôi nghĩ. x.Sin()sẽ có một số điều chỉnh, nhưng tôi lạm dụng các phương pháp mở rộng và cá nhân tôi sẽ là thiên hướng đầu tiên của tôi.
WernerCD

2

C #: Một biến thể trong câu trả lời của Randall Cook , tôi thích vì nó duy trì "giao diện" toán học của mã hơn các phương thức mở rộng, là sử dụng trình bao bọc nhưng sử dụng tham chiếu hàm cho các cuộc gọi thay vì bọc chúng. Cá nhân tôi nghĩ rằng nó làm cho mã trông sạch hơn, nhưng về cơ bản nó đang làm điều tương tự.

Tôi đã gõ một chương trình thử nghiệm LINQPad nhỏ bao gồm các hàm được bọc của Randall, các tham chiếu chức năng của tôi và các cuộc gọi trực tiếp.

Các cuộc gọi tham chiếu chức năng về cơ bản mất cùng thời gian với các cuộc gọi trực tiếp. Các chức năng được bọc luôn chậm hơn - mặc dù không phải là một số lượng lớn.

Đây là mã:

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);
    }
}

Các kết quả:

Function reference:
00:00:06.5952113

Wrapped function:
00:00:07.2570828

Direct call:
00:00:06.6396096

1

Sử dụng Scala! Bạn có thể xác định các toán tử tượng trưng và bạn không cần parens cho các phương thức của mình. Điều này làm cho cách toán dễ dàng hơn để giải thích.

Ví dụ, phép tính tương tự trong Scala và Java có thể giống như:

// 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));
}

Điều này cộng lại khá nhanh.


2
Scala không có sẵn trên CLR, chỉ có JVM. Do đó, nó không thực sự là một sự thay thế khả thi cho C #.
ben rudgers

@benrudgers - C # không chạy trên JVM, vì vậy nó không thực sự là một sự thay thế khả thi cho Java, câu hỏi cũng được hỏi về. Câu hỏi không xác định rằng nó phải là CLR!
Rex Kerr

Có thể tôi là một Luddite, nhưng hai ký tự phụ cho "dấu chấm" thay vì "*", với lợi ích là mã rõ ràng hơn, có vẻ là một cái giá nhỏ phải trả. Tuy nhiên, một câu trả lời tốt.
dùng949300
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.